agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v28 07/11] Add DISTINCT support for IVM 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ 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; 381+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if 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] 381+ messages in thread
* [PATCH v19 1/6] Split vacuumdb to create vacuuming.c/h @ 2025-08-30 12:39 Álvaro Herrera <[email protected]> 0 siblings, 0 replies; 381+ messages in thread From: Álvaro Herrera @ 2025-08-30 12:39 UTC (permalink / raw) --- src/bin/scripts/Makefile | 4 +- src/bin/scripts/meson.build | 28 +- src/bin/scripts/vacuumdb.c | 1048 +---------------------------------- src/bin/scripts/vacuuming.c | 978 ++++++++++++++++++++++++++++++++ src/bin/scripts/vacuuming.h | 95 ++++ 5 files changed, 1119 insertions(+), 1034 deletions(-) create mode 100644 src/bin/scripts/vacuuming.c create mode 100644 src/bin/scripts/vacuuming.h diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile index f6b4d40810b..019ca06455d 100644 --- a/src/bin/scripts/Makefile +++ b/src/bin/scripts/Makefile @@ -28,7 +28,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils -vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils +vacuumdb: vacuumdb.o vacuuming.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils @@ -50,7 +50,7 @@ uninstall: clean distclean: rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS)) - rm -f common.o $(WIN32RES) + rm -f common.o vacuuming.o $(WIN32RES) rm -rf tmp_check export with_icu diff --git a/src/bin/scripts/meson.build b/src/bin/scripts/meson.build index 80df7c33257..a4fed59d1c9 100644 --- a/src/bin/scripts/meson.build +++ b/src/bin/scripts/meson.build @@ -12,7 +12,6 @@ binaries = [ 'createuser', 'dropuser', 'clusterdb', - 'vacuumdb', 'reindexdb', 'pg_isready', ] @@ -35,6 +34,33 @@ foreach binary : binaries bin_targets += binary endforeach +vacuuming_common = static_library('libvacuuming_common', + files('common.c', 'vacuuming.c'), + dependencies: [frontend_code, libpq], + kwargs: internal_lib_args, +) + +binaries = [ + 'vacuumdb', +] +foreach binary : binaries + binary_sources = files('@[email protected]'.format(binary)) + + if host_system == 'windows' + binary_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', binary, + '--FILEDESC', '@0@ - PostgreSQL utility'.format(binary),]) + endif + + binary = executable(binary, + binary_sources, + link_with: [vacuuming_common], + dependencies: [frontend_code, libpq], + kwargs: default_bin_args, + ) + bin_targets += binary +endforeach + tests += { 'name': 'scripts', 'sd': meson.current_source_dir(), diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c index fd236087e90..b1be61ddf25 100644 --- a/src/bin/scripts/vacuumdb.c +++ b/src/bin/scripts/vacuumdb.c @@ -14,92 +14,13 @@ #include <limits.h> -#include "catalog/pg_attribute_d.h" -#include "catalog/pg_class_d.h" #include "common.h" -#include "common/connect.h" #include "common/logging.h" -#include "fe_utils/cancel.h" #include "fe_utils/option_utils.h" -#include "fe_utils/parallel_slot.h" -#include "fe_utils/query_utils.h" -#include "fe_utils/simple_list.h" -#include "fe_utils/string_utils.h" - - -/* vacuum options controlled by user flags */ -typedef struct vacuumingOptions -{ - bool analyze_only; - bool verbose; - bool and_analyze; - bool full; - bool freeze; - bool disable_page_skipping; - bool skip_locked; - int min_xid_age; - int min_mxid_age; - int parallel_workers; /* >= 0 indicates user specified the - * parallel degree, otherwise -1 */ - bool no_index_cleanup; - bool force_index_cleanup; - bool do_truncate; - bool process_main; - bool process_toast; - bool skip_database_stats; - char *buffer_usage_limit; - bool missing_stats_only; -} vacuumingOptions; - -/* object filter options */ -typedef enum -{ - OBJFILTER_NONE = 0, /* no filter used */ - OBJFILTER_ALL_DBS = (1 << 0), /* -a | --all */ - OBJFILTER_DATABASE = (1 << 1), /* -d | --dbname */ - OBJFILTER_TABLE = (1 << 2), /* -t | --table */ - OBJFILTER_SCHEMA = (1 << 3), /* -n | --schema */ - OBJFILTER_SCHEMA_EXCLUDE = (1 << 4), /* -N | --exclude-schema */ -} VacObjFilter; - -static VacObjFilter objfilter = OBJFILTER_NONE; - -static SimpleStringList *retrieve_objects(PGconn *conn, - vacuumingOptions *vacopts, - SimpleStringList *objects, - bool echo); - -static void vacuum_one_database(ConnParams *cparams, - vacuumingOptions *vacopts, - int stage, - SimpleStringList *objects, - SimpleStringList **found_objs, - int concurrentCons, - const char *progname, bool echo, bool quiet); - -static void vacuum_all_databases(ConnParams *cparams, - vacuumingOptions *vacopts, - bool analyze_in_stages, - SimpleStringList *objects, - int concurrentCons, - const char *progname, bool echo, bool quiet); - -static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion, - vacuumingOptions *vacopts, const char *table); - -static void run_vacuum_command(PGconn *conn, const char *sql, bool echo, - const char *table); +#include "vacuuming.h" static void help(const char *progname); - -void check_objfilter(void); - -static char *escape_quotes(const char *src); - -/* For analyze-in-stages mode */ -#define ANALYZE_NO_STAGE -1 -#define ANALYZE_NUM_STAGES 3 - +static void check_objfilter(void); int main(int argc, char *argv[]) @@ -145,10 +66,6 @@ main(int argc, char *argv[]) int c; const char *dbname = NULL; const char *maintenance_db = NULL; - char *host = NULL; - char *port = NULL; - char *username = NULL; - enum trivalue prompt_password = TRI_DEFAULT; ConnParams cparams; bool echo = false; bool quiet = false; @@ -168,13 +85,18 @@ main(int argc, char *argv[]) vacopts.process_main = true; vacopts.process_toast = true; + /* the same for connection parameters */ + memset(&cparams, 0, sizeof(cparams)); + cparams.prompt_password = TRI_DEFAULT; + pg_logging_init(argv[0]); progname = get_progname(argv[0]); set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts")); - handle_help_version_opts(argc, argv, "vacuumdb", help); + handle_help_version_opts(argc, argv, progname, help); - while ((c = getopt_long(argc, argv, "ad:efFh:j:n:N:p:P:qt:U:vwWzZ", long_options, &optindex)) != -1) + while ((c = getopt_long(argc, argv, "ad:efFh:j:n:N:p:P:qt:U:vwWzZ", + long_options, &optindex)) != -1) { switch (c) { @@ -195,7 +117,7 @@ main(int argc, char *argv[]) vacopts.freeze = true; break; case 'h': - host = pg_strdup(optarg); + cparams.pghost = pg_strdup(optarg); break; case 'j': if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX, @@ -211,7 +133,7 @@ main(int argc, char *argv[]) simple_string_list_append(&objects, optarg); break; case 'p': - port = pg_strdup(optarg); + cparams.pgport = pg_strdup(optarg); break; case 'P': if (!option_parse_int(optarg, "-P/--parallel", 0, INT_MAX, @@ -227,16 +149,16 @@ main(int argc, char *argv[]) tbl_count++; break; case 'U': - username = pg_strdup(optarg); + cparams.pguser = pg_strdup(optarg); break; case 'v': vacopts.verbose = true; break; case 'w': - prompt_password = TRI_NO; + cparams.prompt_password = TRI_NO; break; case 'W': - prompt_password = TRI_YES; + cparams.prompt_password = TRI_YES; break; case 'z': vacopts.and_analyze = true; @@ -380,66 +302,9 @@ main(int argc, char *argv[]) pg_fatal("cannot use the \"%s\" option without \"%s\" or \"%s\"", "missing-stats-only", "analyze-only", "analyze-in-stages"); - /* fill cparams except for dbname, which is set below */ - cparams.pghost = host; - cparams.pgport = port; - cparams.pguser = username; - cparams.prompt_password = prompt_password; - cparams.override_dbname = NULL; - - setup_cancel_handler(NULL); - - /* Avoid opening extra connections. */ - if (tbl_count && (concurrentCons > tbl_count)) - concurrentCons = tbl_count; - - if (objfilter & OBJFILTER_ALL_DBS) - { - cparams.dbname = maintenance_db; - - vacuum_all_databases(&cparams, &vacopts, - analyze_in_stages, - &objects, - concurrentCons, - progname, echo, quiet); - } - else - { - if (dbname == NULL) - { - if (getenv("PGDATABASE")) - dbname = getenv("PGDATABASE"); - else if (getenv("PGUSER")) - dbname = getenv("PGUSER"); - else - dbname = get_user_name_or_exit(progname); - } - - cparams.dbname = dbname; - - if (analyze_in_stages) - { - int stage; - SimpleStringList *found_objs = NULL; - - for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) - { - vacuum_one_database(&cparams, &vacopts, - stage, - &objects, - vacopts.missing_stats_only ? &found_objs : NULL, - concurrentCons, - progname, echo, quiet); - } - } - else - vacuum_one_database(&cparams, &vacopts, - ANALYZE_NO_STAGE, - &objects, NULL, - concurrentCons, - progname, echo, quiet); - } - + vacuuming_main(&cparams, dbname, maintenance_db, &vacopts, &objects, + analyze_in_stages, tbl_count, concurrentCons, + progname, echo, quiet); exit(0); } @@ -466,885 +331,6 @@ check_objfilter(void) pg_fatal("cannot vacuum all tables in schema(s) and exclude schema(s) at the same time"); } -/* - * Returns a newly malloc'd version of 'src' with escaped single quotes and - * backslashes. - */ -static char * -escape_quotes(const char *src) -{ - char *result = escape_single_quotes_ascii(src); - - if (!result) - pg_fatal("out of memory"); - return result; -} - -/* - * vacuum_one_database - * - * Process tables in the given database. - * - * There are two ways to specify the list of objects to process: - * - * 1) The "found_objs" parameter is a double pointer to a fully qualified list - * of objects to process, as returned by a previous call to - * vacuum_one_database(). - * - * a) If both "found_objs" (the double pointer) and "*found_objs" (the - * once-dereferenced double pointer) are not NULL, this list takes - * priority, and anything specified in "objects" is ignored. - * - * b) If "found_objs" (the double pointer) is not NULL but "*found_objs" - * (the once-dereferenced double pointer) _is_ NULL, the "objects" - * parameter takes priority, and the results of the catalog query - * described in (2) are stored in "found_objs". - * - * c) If "found_objs" (the double pointer) is NULL, the "objects" - * parameter again takes priority, and the results of the catalog query - * are not saved. - * - * 2) The "objects" parameter is a user-specified list of objects to process. - * When (1b) or (1c) applies, this function performs a catalog query to - * retrieve a fully qualified list of objects to process, as described - * below. - * - * a) If "objects" is not NULL, the catalog query gathers only the objects - * listed in "objects". - * - * b) If "objects" is NULL, all tables in the database are gathered. - * - * Note that this function is only concerned with running exactly one stage - * when in analyze-in-stages mode; caller must iterate on us if necessary. - * - * If concurrentCons is > 1, multiple connections are used to vacuum tables - * in parallel. - */ -static void -vacuum_one_database(ConnParams *cparams, - vacuumingOptions *vacopts, - int stage, - SimpleStringList *objects, - SimpleStringList **found_objs, - int concurrentCons, - const char *progname, bool echo, bool quiet) -{ - PQExpBufferData sql; - PGconn *conn; - SimpleStringListCell *cell; - ParallelSlotArray *sa; - int ntups = 0; - bool failed = false; - const char *initcmd; - SimpleStringList *ret = NULL; - const char *stage_commands[] = { - "SET default_statistics_target=1; SET vacuum_cost_delay=0;", - "SET default_statistics_target=10; RESET vacuum_cost_delay;", - "RESET default_statistics_target;" - }; - const char *stage_messages[] = { - gettext_noop("Generating minimal optimizer statistics (1 target)"), - gettext_noop("Generating medium optimizer statistics (10 targets)"), - gettext_noop("Generating default (full) optimizer statistics") - }; - - Assert(stage == ANALYZE_NO_STAGE || - (stage >= 0 && stage < ANALYZE_NUM_STAGES)); - - conn = connectDatabase(cparams, progname, echo, false, true); - - if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "disable-page-skipping", "9.6"); - } - - if (vacopts->no_index_cleanup && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-index-cleanup", "12"); - } - - if (vacopts->force_index_cleanup && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "force-index-cleanup", "12"); - } - - if (!vacopts->do_truncate && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-truncate", "12"); - } - - if (!vacopts->process_main && PQserverVersion(conn) < 160000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-process-main", "16"); - } - - if (!vacopts->process_toast && PQserverVersion(conn) < 140000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-process-toast", "14"); - } - - if (vacopts->skip_locked && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "skip-locked", "12"); - } - - if (vacopts->min_xid_age != 0 && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--min-xid-age", "9.6"); - } - - if (vacopts->min_mxid_age != 0 && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--min-mxid-age", "9.6"); - } - - if (vacopts->parallel_workers >= 0 && PQserverVersion(conn) < 130000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--parallel", "13"); - } - - if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--buffer-usage-limit", "16"); - } - - if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--missing-stats-only", "15"); - } - - /* skip_database_stats is used automatically if server supports it */ - vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000); - - if (!quiet) - { - if (stage != ANALYZE_NO_STAGE) - printf(_("%s: processing database \"%s\": %s\n"), - progname, PQdb(conn), _(stage_messages[stage])); - else - printf(_("%s: vacuuming database \"%s\"\n"), - progname, PQdb(conn)); - fflush(stdout); - } - - /* - * If the caller provided the results of a previous catalog query, just - * use that. Otherwise, run the catalog query ourselves and set the - * return variable if provided. - */ - if (found_objs && *found_objs) - ret = *found_objs; - else - { - ret = retrieve_objects(conn, vacopts, objects, echo); - if (found_objs) - *found_objs = ret; - } - - /* - * Count the number of objects in the catalog query result. If there are - * none, we are done. - */ - for (cell = ret ? ret->head : NULL; cell; cell = cell->next) - ntups++; - - if (ntups == 0) - { - PQfinish(conn); - return; - } - - /* - * Ensure concurrentCons is sane. If there are more connections than - * vacuumable relations, we don't need to use them all. - */ - if (concurrentCons > ntups) - concurrentCons = ntups; - if (concurrentCons <= 0) - concurrentCons = 1; - - /* - * All slots need to be prepared to run the appropriate analyze stage, if - * caller requested that mode. We have to prepare the initial connection - * ourselves before setting up the slots. - */ - if (stage == ANALYZE_NO_STAGE) - initcmd = NULL; - else - { - initcmd = stage_commands[stage]; - executeCommand(conn, initcmd, echo); - } - - /* - * Setup the database connections. We reuse the connection we already have - * for the first slot. If not in parallel mode, the first slot in the - * array contains the connection. - */ - sa = ParallelSlotsSetup(concurrentCons, cparams, progname, echo, initcmd); - ParallelSlotsAdoptConn(sa, conn); - - initPQExpBuffer(&sql); - - cell = ret->head; - do - { - const char *tabname = cell->val; - ParallelSlot *free_slot; - - if (CancelRequested) - { - failed = true; - goto finish; - } - - free_slot = ParallelSlotsGetIdle(sa, NULL); - if (!free_slot) - { - failed = true; - goto finish; - } - - prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection), - vacopts, tabname); - - /* - * Execute the vacuum. All errors are handled in processQueryResult - * through ParallelSlotsGetIdle. - */ - ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); - run_vacuum_command(free_slot->connection, sql.data, - echo, tabname); - - cell = cell->next; - } while (cell != NULL); - - if (!ParallelSlotsWaitCompletion(sa)) - { - failed = true; - goto finish; - } - - /* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */ - if (vacopts->skip_database_stats && stage == ANALYZE_NO_STAGE) - { - const char *cmd = "VACUUM (ONLY_DATABASE_STATS);"; - ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL); - - if (!free_slot) - { - failed = true; - goto finish; - } - - ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); - run_vacuum_command(free_slot->connection, cmd, echo, NULL); - - if (!ParallelSlotsWaitCompletion(sa)) - failed = true; - } - -finish: - ParallelSlotsTerminate(sa); - pg_free(sa); - - termPQExpBuffer(&sql); - - if (failed) - exit(1); -} - -/* - * Prepare the list of tables to process by querying the catalogs. - * - * Since we execute the constructed query with the default search_path (which - * could be unsafe), everything in this query MUST be fully qualified. - * - * First, build a WITH clause for the catalog query if any tables were - * specified, with a set of values made of relation names and their optional - * set of columns. This is used to match any provided column lists with the - * generated qualified identifiers and to filter for the tables provided via - * --table. If a listed table does not exist, the catalog query will fail. - */ -static SimpleStringList * -retrieve_objects(PGconn *conn, vacuumingOptions *vacopts, - SimpleStringList *objects, bool echo) -{ - PQExpBufferData buf; - PQExpBufferData catalog_query; - PGresult *res; - SimpleStringListCell *cell; - SimpleStringList *found_objs = palloc0(sizeof(SimpleStringList)); - bool objects_listed = false; - - initPQExpBuffer(&catalog_query); - for (cell = objects ? objects->head : NULL; cell; cell = cell->next) - { - char *just_table = NULL; - const char *just_columns = NULL; - - if (!objects_listed) - { - appendPQExpBufferStr(&catalog_query, - "WITH listed_objects (object_oid, column_list) " - "AS (\n VALUES ("); - objects_listed = true; - } - else - appendPQExpBufferStr(&catalog_query, ",\n ("); - - if (objfilter & (OBJFILTER_SCHEMA | OBJFILTER_SCHEMA_EXCLUDE)) - { - appendStringLiteralConn(&catalog_query, cell->val, conn); - appendPQExpBufferStr(&catalog_query, "::pg_catalog.regnamespace, "); - } - - if (objfilter & OBJFILTER_TABLE) - { - /* - * Split relation and column names given by the user, this is used - * to feed the CTE with values on which are performed pre-run - * validity checks as well. For now these happen only on the - * relation name. - */ - splitTableColumnsSpec(cell->val, PQclientEncoding(conn), - &just_table, &just_columns); - - appendStringLiteralConn(&catalog_query, just_table, conn); - appendPQExpBufferStr(&catalog_query, "::pg_catalog.regclass, "); - } - - if (just_columns && just_columns[0] != '\0') - appendStringLiteralConn(&catalog_query, just_columns, conn); - else - appendPQExpBufferStr(&catalog_query, "NULL"); - - appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)"); - - pg_free(just_table); - } - - /* Finish formatting the CTE */ - if (objects_listed) - appendPQExpBufferStr(&catalog_query, "\n)\n"); - - appendPQExpBufferStr(&catalog_query, "SELECT c.relname, ns.nspname"); - - if (objects_listed) - appendPQExpBufferStr(&catalog_query, ", listed_objects.column_list"); - - appendPQExpBufferStr(&catalog_query, - " FROM pg_catalog.pg_class c\n" - " JOIN pg_catalog.pg_namespace ns" - " ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n" - " CROSS JOIN LATERAL (SELECT c.relkind IN (" - CppAsString2(RELKIND_PARTITIONED_TABLE) ", " - CppAsString2(RELKIND_PARTITIONED_INDEX) ")) as p (inherited)\n" - " LEFT JOIN pg_catalog.pg_class t" - " ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid\n"); - - /* - * Used to match the tables or schemas listed by the user, completing the - * JOIN clause. - */ - if (objects_listed) - { - appendPQExpBufferStr(&catalog_query, " LEFT JOIN listed_objects" - " ON listed_objects.object_oid" - " OPERATOR(pg_catalog.=) "); - - if (objfilter & OBJFILTER_TABLE) - appendPQExpBufferStr(&catalog_query, "c.oid\n"); - else - appendPQExpBufferStr(&catalog_query, "ns.oid\n"); - } - - /* - * Exclude temporary tables, beginning the WHERE clause. - */ - appendPQExpBufferStr(&catalog_query, - " WHERE c.relpersistence OPERATOR(pg_catalog.!=) " - CppAsString2(RELPERSISTENCE_TEMP) "\n"); - - /* - * Used to match the tables or schemas listed by the user, for the WHERE - * clause. - */ - if (objects_listed) - { - if (objfilter & OBJFILTER_SCHEMA_EXCLUDE) - appendPQExpBufferStr(&catalog_query, - " AND listed_objects.object_oid IS NULL\n"); - else - appendPQExpBufferStr(&catalog_query, - " AND listed_objects.object_oid IS NOT NULL\n"); - } - - /* - * If no tables were listed, filter for the relevant relation types. If - * tables were given via --table, don't bother filtering by relation type. - * Instead, let the server decide whether a given relation can be - * processed in which case the user will know about it. - */ - if ((objfilter & OBJFILTER_TABLE) == 0) - { - /* - * vacuumdb should generally follow the behavior of the underlying - * VACUUM and ANALYZE commands. If analyze_only is true, process - * regular tables, materialized views, and partitioned tables, just - * like ANALYZE (with no specific target tables) does. Otherwise, - * process only regular tables and materialized views, since VACUUM - * skips partitioned tables when no target tables are specified. - */ - if (vacopts->analyze_only) - appendPQExpBufferStr(&catalog_query, - " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" - CppAsString2(RELKIND_RELATION) ", " - CppAsString2(RELKIND_MATVIEW) ", " - CppAsString2(RELKIND_PARTITIONED_TABLE) "])\n"); - else - appendPQExpBufferStr(&catalog_query, - " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" - CppAsString2(RELKIND_RELATION) ", " - CppAsString2(RELKIND_MATVIEW) "])\n"); - - } - - /* - * For --min-xid-age and --min-mxid-age, the age of the relation is the - * greatest of the ages of the main relation and its associated TOAST - * table. The commands generated by vacuumdb will also process the TOAST - * table for the relation if necessary, so it does not need to be - * considered separately. - */ - if (vacopts->min_xid_age != 0) - { - appendPQExpBuffer(&catalog_query, - " AND GREATEST(pg_catalog.age(c.relfrozenxid)," - " pg_catalog.age(t.relfrozenxid)) " - " OPERATOR(pg_catalog.>=) '%d'::pg_catalog.int4\n" - " AND c.relfrozenxid OPERATOR(pg_catalog.!=)" - " '0'::pg_catalog.xid\n", - vacopts->min_xid_age); - } - - if (vacopts->min_mxid_age != 0) - { - appendPQExpBuffer(&catalog_query, - " AND GREATEST(pg_catalog.mxid_age(c.relminmxid)," - " pg_catalog.mxid_age(t.relminmxid)) OPERATOR(pg_catalog.>=)" - " '%d'::pg_catalog.int4\n" - " AND c.relminmxid OPERATOR(pg_catalog.!=)" - " '0'::pg_catalog.xid\n", - vacopts->min_mxid_age); - } - - if (vacopts->missing_stats_only) - { - appendPQExpBufferStr(&catalog_query, " AND (\n"); - - /* regular stats */ - appendPQExpBufferStr(&catalog_query, - " EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND a.attgenerated OPERATOR(pg_catalog.<>) " - CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* extended stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" - " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" - " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" - " AND d.stxdinherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* expression indexes */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " JOIN pg_catalog.pg_index i" - " ON i.indexrelid OPERATOR(pg_catalog.=) a.attrelid\n" - " WHERE i.indrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND i.indkey[a.attnum OPERATOR(pg_catalog.-) 1::pg_catalog.int2]" - " OPERATOR(pg_catalog.=) 0::pg_catalog.int2\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* inheritance and regular stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND a.attgenerated OPERATOR(pg_catalog.<>) " - CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" - " AND c.relhassubclass\n" - " AND NOT p.inherited\n" - " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" - " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit))\n"); - - /* inheritance and extended stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" - " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND c.relhassubclass\n" - " AND NOT p.inherited\n" - " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" - " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" - " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" - " AND d.stxdinherit))\n"); - - appendPQExpBufferStr(&catalog_query, " )\n"); - } - - /* - * Execute the catalog query. We use the default search_path for this - * query for consistency with table lookups done elsewhere by the user. - */ - appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;"); - executeCommand(conn, "RESET search_path;", echo); - res = executeQuery(conn, catalog_query.data, echo); - termPQExpBuffer(&catalog_query); - PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo)); - - /* - * Build qualified identifiers for each table, including the column list - * if given. - */ - initPQExpBuffer(&buf); - for (int i = 0; i < PQntuples(res); i++) - { - appendPQExpBufferStr(&buf, - fmtQualifiedIdEnc(PQgetvalue(res, i, 1), - PQgetvalue(res, i, 0), - PQclientEncoding(conn))); - - if (objects_listed && !PQgetisnull(res, i, 2)) - appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2)); - - simple_string_list_append(found_objs, buf.data); - resetPQExpBuffer(&buf); - } - termPQExpBuffer(&buf); - PQclear(res); - - return found_objs; -} - -/* - * Vacuum/analyze all connectable databases. - * - * In analyze-in-stages mode, we process all databases in one stage before - * moving on to the next stage. That ensure minimal stats are available - * quickly everywhere before generating more detailed ones. - */ -static void -vacuum_all_databases(ConnParams *cparams, - vacuumingOptions *vacopts, - bool analyze_in_stages, - SimpleStringList *objects, - int concurrentCons, - const char *progname, bool echo, bool quiet) -{ - PGconn *conn; - PGresult *result; - int stage; - int i; - - conn = connectMaintenanceDatabase(cparams, progname, echo); - result = executeQuery(conn, - "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", - echo); - PQfinish(conn); - - if (analyze_in_stages) - { - SimpleStringList **found_objs = NULL; - - if (vacopts->missing_stats_only) - found_objs = palloc0(PQntuples(result) * sizeof(SimpleStringList *)); - - /* - * When analyzing all databases in stages, we analyze them all in the - * fastest stage first, so that initial statistics become available - * for all of them as soon as possible. - * - * This means we establish several times as many connections, but - * that's a secondary consideration. - */ - for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) - { - for (i = 0; i < PQntuples(result); i++) - { - cparams->override_dbname = PQgetvalue(result, i, 0); - - vacuum_one_database(cparams, vacopts, - stage, - objects, - vacopts->missing_stats_only ? &found_objs[i] : NULL, - concurrentCons, - progname, echo, quiet); - } - } - } - else - { - for (i = 0; i < PQntuples(result); i++) - { - cparams->override_dbname = PQgetvalue(result, i, 0); - - vacuum_one_database(cparams, vacopts, - ANALYZE_NO_STAGE, - objects, NULL, - concurrentCons, - progname, echo, quiet); - } - } - - PQclear(result); -} - -/* - * Construct a vacuum/analyze command to run based on the given options, in the - * given string buffer, which may contain previous garbage. - * - * The table name used must be already properly quoted. The command generated - * depends on the server version involved and it is semicolon-terminated. - */ -static void -prepare_vacuum_command(PQExpBuffer sql, int serverVersion, - vacuumingOptions *vacopts, const char *table) -{ - const char *paren = " ("; - const char *comma = ", "; - const char *sep = paren; - - resetPQExpBuffer(sql); - - if (vacopts->analyze_only) - { - appendPQExpBufferStr(sql, "ANALYZE"); - - /* parenthesized grammar of ANALYZE is supported since v11 */ - if (serverVersion >= 110000) - { - if (vacopts->skip_locked) - { - /* SKIP_LOCKED is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); - sep = comma; - } - if (vacopts->verbose) - { - appendPQExpBuffer(sql, "%sVERBOSE", sep); - sep = comma; - } - if (vacopts->buffer_usage_limit) - { - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, - vacopts->buffer_usage_limit); - sep = comma; - } - if (sep != paren) - appendPQExpBufferChar(sql, ')'); - } - else - { - if (vacopts->verbose) - appendPQExpBufferStr(sql, " VERBOSE"); - } - } - else - { - appendPQExpBufferStr(sql, "VACUUM"); - - /* parenthesized grammar of VACUUM is supported since v9.0 */ - if (serverVersion >= 90000) - { - if (vacopts->disable_page_skipping) - { - /* DISABLE_PAGE_SKIPPING is supported since v9.6 */ - Assert(serverVersion >= 90600); - appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep); - sep = comma; - } - if (vacopts->no_index_cleanup) - { - /* "INDEX_CLEANUP FALSE" has been supported since v12 */ - Assert(serverVersion >= 120000); - Assert(!vacopts->force_index_cleanup); - appendPQExpBuffer(sql, "%sINDEX_CLEANUP FALSE", sep); - sep = comma; - } - if (vacopts->force_index_cleanup) - { - /* "INDEX_CLEANUP TRUE" has been supported since v12 */ - Assert(serverVersion >= 120000); - Assert(!vacopts->no_index_cleanup); - appendPQExpBuffer(sql, "%sINDEX_CLEANUP TRUE", sep); - sep = comma; - } - if (!vacopts->do_truncate) - { - /* TRUNCATE is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep); - sep = comma; - } - if (!vacopts->process_main) - { - /* PROCESS_MAIN is supported since v16 */ - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep); - sep = comma; - } - if (!vacopts->process_toast) - { - /* PROCESS_TOAST is supported since v14 */ - Assert(serverVersion >= 140000); - appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep); - sep = comma; - } - if (vacopts->skip_database_stats) - { - /* SKIP_DATABASE_STATS is supported since v16 */ - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep); - sep = comma; - } - if (vacopts->skip_locked) - { - /* SKIP_LOCKED is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); - sep = comma; - } - if (vacopts->full) - { - appendPQExpBuffer(sql, "%sFULL", sep); - sep = comma; - } - if (vacopts->freeze) - { - appendPQExpBuffer(sql, "%sFREEZE", sep); - sep = comma; - } - if (vacopts->verbose) - { - appendPQExpBuffer(sql, "%sVERBOSE", sep); - sep = comma; - } - if (vacopts->and_analyze) - { - appendPQExpBuffer(sql, "%sANALYZE", sep); - sep = comma; - } - if (vacopts->parallel_workers >= 0) - { - /* PARALLEL is supported since v13 */ - Assert(serverVersion >= 130000); - appendPQExpBuffer(sql, "%sPARALLEL %d", sep, - vacopts->parallel_workers); - sep = comma; - } - if (vacopts->buffer_usage_limit) - { - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, - vacopts->buffer_usage_limit); - sep = comma; - } - if (sep != paren) - appendPQExpBufferChar(sql, ')'); - } - else - { - if (vacopts->full) - appendPQExpBufferStr(sql, " FULL"); - if (vacopts->freeze) - appendPQExpBufferStr(sql, " FREEZE"); - if (vacopts->verbose) - appendPQExpBufferStr(sql, " VERBOSE"); - if (vacopts->and_analyze) - appendPQExpBufferStr(sql, " ANALYZE"); - } - } - - appendPQExpBuffer(sql, " %s;", table); -} - -/* - * Send a vacuum/analyze command to the server, returning after sending the - * command. - * - * Any errors during command execution are reported to stderr. - */ -static void -run_vacuum_command(PGconn *conn, const char *sql, bool echo, - const char *table) -{ - bool status; - - if (echo) - printf("%s\n", sql); - - status = PQsendQuery(conn, sql) == 1; - - if (!status) - { - if (table) - pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", - table, PQdb(conn), PQerrorMessage(conn)); - else - pg_log_error("vacuuming of database \"%s\" failed: %s", - PQdb(conn), PQerrorMessage(conn)); - } -} static void help(const char *progname) diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c new file mode 100644 index 00000000000..9be37fcc45a --- /dev/null +++ b/src/bin/scripts/vacuuming.c @@ -0,0 +1,978 @@ +/*------------------------------------------------------------------------- + * vacuuming.c + * Common routines for vacuumdb + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/bin/scripts/vacuuming.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <limits.h> + +#include "catalog/pg_attribute_d.h" +#include "catalog/pg_class_d.h" +#include "common/connect.h" +#include "common/logging.h" +#include "fe_utils/cancel.h" +#include "fe_utils/option_utils.h" +#include "fe_utils/parallel_slot.h" +#include "fe_utils/query_utils.h" +#include "fe_utils/string_utils.h" +#include "vacuuming.h" + +VacObjFilter objfilter = OBJFILTER_NONE; + + +/* + * Executes vacuum/analyze as indicated, or dies in case of failure. + */ +void +vacuuming_main(ConnParams *cparams, const char *dbname, + const char *maintenance_db, vacuumingOptions *vacopts, + SimpleStringList *objects, bool analyze_in_stages, + int tbl_count, int concurrentCons, + const char *progname, bool echo, bool quiet) +{ + setup_cancel_handler(NULL); + + /* Avoid opening extra connections. */ + if (tbl_count && (concurrentCons > tbl_count)) + concurrentCons = tbl_count; + + if (objfilter & OBJFILTER_ALL_DBS) + { + cparams->dbname = maintenance_db; + + vacuum_all_databases(cparams, vacopts, + analyze_in_stages, + objects, + concurrentCons, + progname, echo, quiet); + } + else + { + if (dbname == NULL) + { + if (getenv("PGDATABASE")) + dbname = getenv("PGDATABASE"); + else if (getenv("PGUSER")) + dbname = getenv("PGUSER"); + else + dbname = get_user_name_or_exit(progname); + } + + cparams->dbname = dbname; + + if (analyze_in_stages) + { + int stage; + SimpleStringList *found_objs = NULL; + + for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) + { + vacuum_one_database(cparams, vacopts, + stage, + objects, + vacopts->missing_stats_only ? &found_objs : NULL, + concurrentCons, + progname, echo, quiet); + } + } + else + vacuum_one_database(cparams, vacopts, + ANALYZE_NO_STAGE, + objects, NULL, + concurrentCons, + progname, echo, quiet); + } +} + + +/* + * vacuum_one_database + * + * Process tables in the given database. + * + * There are two ways to specify the list of objects to process: + * + * 1) The "found_objs" parameter is a double pointer to a fully qualified list + * of objects to process, as returned by a previous call to + * vacuum_one_database(). + * + * a) If both "found_objs" (the double pointer) and "*found_objs" (the + * once-dereferenced double pointer) are not NULL, this list takes + * priority, and anything specified in "objects" is ignored. + * + * b) If "found_objs" (the double pointer) is not NULL but "*found_objs" + * (the once-dereferenced double pointer) _is_ NULL, the "objects" + * parameter takes priority, and the results of the catalog query + * described in (2) are stored in "found_objs". + * + * c) If "found_objs" (the double pointer) is NULL, the "objects" + * parameter again takes priority, and the results of the catalog query + * are not saved. + * + * 2) The "objects" parameter is a user-specified list of objects to process. + * When (1b) or (1c) applies, this function performs a catalog query to + * retrieve a fully qualified list of objects to process, as described + * below. + * + * a) If "objects" is not NULL, the catalog query gathers only the objects + * listed in "objects". + * + * b) If "objects" is NULL, all tables in the database are gathered. + * + * Note that this function is only concerned with running exactly one stage + * when in analyze-in-stages mode; caller must iterate on us if necessary. + * + * If concurrentCons is > 1, multiple connections are used to vacuum tables + * in parallel. + */ +void +vacuum_one_database(ConnParams *cparams, + vacuumingOptions *vacopts, + int stage, + SimpleStringList *objects, + SimpleStringList **found_objs, + int concurrentCons, + const char *progname, bool echo, bool quiet) +{ + PQExpBufferData sql; + PGconn *conn; + SimpleStringListCell *cell; + ParallelSlotArray *sa; + int ntups = 0; + bool failed = false; + const char *initcmd; + SimpleStringList *ret = NULL; + const char *stage_commands[] = { + "SET default_statistics_target=1; SET vacuum_cost_delay=0;", + "SET default_statistics_target=10; RESET vacuum_cost_delay;", + "RESET default_statistics_target;" + }; + const char *stage_messages[] = { + gettext_noop("Generating minimal optimizer statistics (1 target)"), + gettext_noop("Generating medium optimizer statistics (10 targets)"), + gettext_noop("Generating default (full) optimizer statistics") + }; + + Assert(stage == ANALYZE_NO_STAGE || + (stage >= 0 && stage < ANALYZE_NUM_STAGES)); + + conn = connectDatabase(cparams, progname, echo, false, true); + + if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "disable-page-skipping", "9.6"); + } + + if (vacopts->no_index_cleanup && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-index-cleanup", "12"); + } + + if (vacopts->force_index_cleanup && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "force-index-cleanup", "12"); + } + + if (!vacopts->do_truncate && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-truncate", "12"); + } + + if (!vacopts->process_main && PQserverVersion(conn) < 160000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-process-main", "16"); + } + + if (!vacopts->process_toast && PQserverVersion(conn) < 140000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-process-toast", "14"); + } + + if (vacopts->skip_locked && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "skip-locked", "12"); + } + + if (vacopts->min_xid_age != 0 && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--min-xid-age", "9.6"); + } + + if (vacopts->min_mxid_age != 0 && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--min-mxid-age", "9.6"); + } + + if (vacopts->parallel_workers >= 0 && PQserverVersion(conn) < 130000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--parallel", "13"); + } + + if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--buffer-usage-limit", "16"); + } + + if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--missing-stats-only", "15"); + } + + /* skip_database_stats is used automatically if server supports it */ + vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000); + + if (!quiet) + { + if (stage != ANALYZE_NO_STAGE) + printf(_("%s: processing database \"%s\": %s\n"), + progname, PQdb(conn), _(stage_messages[stage])); + else + printf(_("%s: vacuuming database \"%s\"\n"), + progname, PQdb(conn)); + fflush(stdout); + } + + /* + * If the caller provided the results of a previous catalog query, just + * use that. Otherwise, run the catalog query ourselves and set the + * return variable if provided. + */ + if (found_objs && *found_objs) + ret = *found_objs; + else + { + ret = retrieve_objects(conn, vacopts, objects, echo); + if (found_objs) + *found_objs = ret; + } + + /* + * Count the number of objects in the catalog query result. If there are + * none, we are done. + */ + for (cell = ret ? ret->head : NULL; cell; cell = cell->next) + ntups++; + + if (ntups == 0) + { + PQfinish(conn); + return; + } + + /* + * Ensure concurrentCons is sane. If there are more connections than + * vacuumable relations, we don't need to use them all. + */ + if (concurrentCons > ntups) + concurrentCons = ntups; + if (concurrentCons <= 0) + concurrentCons = 1; + + /* + * All slots need to be prepared to run the appropriate analyze stage, if + * caller requested that mode. We have to prepare the initial connection + * ourselves before setting up the slots. + */ + if (stage == ANALYZE_NO_STAGE) + initcmd = NULL; + else + { + initcmd = stage_commands[stage]; + executeCommand(conn, initcmd, echo); + } + + /* + * Setup the database connections. We reuse the connection we already have + * for the first slot. If not in parallel mode, the first slot in the + * array contains the connection. + */ + sa = ParallelSlotsSetup(concurrentCons, cparams, progname, echo, initcmd); + ParallelSlotsAdoptConn(sa, conn); + + initPQExpBuffer(&sql); + + cell = ret->head; + do + { + const char *tabname = cell->val; + ParallelSlot *free_slot; + + if (CancelRequested) + { + failed = true; + goto finish; + } + + free_slot = ParallelSlotsGetIdle(sa, NULL); + if (!free_slot) + { + failed = true; + goto finish; + } + + prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection), + vacopts, tabname); + + /* + * Execute the vacuum. All errors are handled in processQueryResult + * through ParallelSlotsGetIdle. + */ + ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); + run_vacuum_command(free_slot->connection, sql.data, + echo, tabname); + + cell = cell->next; + } while (cell != NULL); + + if (!ParallelSlotsWaitCompletion(sa)) + { + failed = true; + goto finish; + } + + /* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */ + if (vacopts->skip_database_stats && + stage == ANALYZE_NO_STAGE) + { + const char *cmd = "VACUUM (ONLY_DATABASE_STATS);"; + ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL); + + if (!free_slot) + { + failed = true; + goto finish; + } + + ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); + run_vacuum_command(free_slot->connection, cmd, echo, NULL); + + if (!ParallelSlotsWaitCompletion(sa)) + failed = true; + } + +finish: + ParallelSlotsTerminate(sa); + pg_free(sa); + + termPQExpBuffer(&sql); + + if (failed) + exit(1); +} + +/* + * Prepare the list of tables to process by querying the catalogs. + * + * Since we execute the constructed query with the default search_path (which + * could be unsafe), everything in this query MUST be fully qualified. + * + * First, build a WITH clause for the catalog query if any tables were + * specified, with a set of values made of relation names and their optional + * set of columns. This is used to match any provided column lists with the + * generated qualified identifiers and to filter for the tables provided via + * --table. If a listed table does not exist, the catalog query will fail. + */ +SimpleStringList * +retrieve_objects(PGconn *conn, vacuumingOptions *vacopts, + SimpleStringList *objects, bool echo) +{ + PQExpBufferData buf; + PQExpBufferData catalog_query; + PGresult *res; + SimpleStringListCell *cell; + SimpleStringList *found_objs = palloc0(sizeof(SimpleStringList)); + bool objects_listed = false; + + initPQExpBuffer(&catalog_query); + for (cell = objects ? objects->head : NULL; cell; cell = cell->next) + { + char *just_table = NULL; + const char *just_columns = NULL; + + if (!objects_listed) + { + appendPQExpBufferStr(&catalog_query, + "WITH listed_objects (object_oid, column_list) AS (\n" + " VALUES ("); + objects_listed = true; + } + else + appendPQExpBufferStr(&catalog_query, ",\n ("); + + if (objfilter & (OBJFILTER_SCHEMA | OBJFILTER_SCHEMA_EXCLUDE)) + { + appendStringLiteralConn(&catalog_query, cell->val, conn); + appendPQExpBufferStr(&catalog_query, "::pg_catalog.regnamespace, "); + } + + if (objfilter & OBJFILTER_TABLE) + { + /* + * Split relation and column names given by the user, this is used + * to feed the CTE with values on which are performed pre-run + * validity checks as well. For now these happen only on the + * relation name. + */ + splitTableColumnsSpec(cell->val, PQclientEncoding(conn), + &just_table, &just_columns); + + appendStringLiteralConn(&catalog_query, just_table, conn); + appendPQExpBufferStr(&catalog_query, "::pg_catalog.regclass, "); + } + + if (just_columns && just_columns[0] != '\0') + appendStringLiteralConn(&catalog_query, just_columns, conn); + else + appendPQExpBufferStr(&catalog_query, "NULL"); + + appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)"); + + pg_free(just_table); + } + + /* Finish formatting the CTE */ + if (objects_listed) + appendPQExpBufferStr(&catalog_query, "\n)\n"); + + appendPQExpBufferStr(&catalog_query, "SELECT c.relname, ns.nspname"); + + if (objects_listed) + appendPQExpBufferStr(&catalog_query, ", listed_objects.column_list"); + + appendPQExpBufferStr(&catalog_query, + " FROM pg_catalog.pg_class c\n" + " JOIN pg_catalog.pg_namespace ns" + " ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n" + " CROSS JOIN LATERAL (SELECT c.relkind IN (" + CppAsString2(RELKIND_PARTITIONED_TABLE) ", " + CppAsString2(RELKIND_PARTITIONED_INDEX) ")) as p (inherited)\n" + " LEFT JOIN pg_catalog.pg_class t" + " ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid\n"); + + /* + * Used to match the tables or schemas listed by the user, completing the + * JOIN clause. + */ + if (objects_listed) + { + appendPQExpBufferStr(&catalog_query, " LEFT JOIN listed_objects" + " ON listed_objects.object_oid" + " OPERATOR(pg_catalog.=) "); + + if (objfilter & OBJFILTER_TABLE) + appendPQExpBufferStr(&catalog_query, "c.oid\n"); + else + appendPQExpBufferStr(&catalog_query, "ns.oid\n"); + } + + /* + * Exclude temporary tables, beginning the WHERE clause. + */ + appendPQExpBufferStr(&catalog_query, + " WHERE c.relpersistence OPERATOR(pg_catalog.!=) " + CppAsString2(RELPERSISTENCE_TEMP) "\n"); + + /* + * Used to match the tables or schemas listed by the user, for the WHERE + * clause. + */ + if (objects_listed) + { + if (objfilter & OBJFILTER_SCHEMA_EXCLUDE) + appendPQExpBufferStr(&catalog_query, + " AND listed_objects.object_oid IS NULL\n"); + else + appendPQExpBufferStr(&catalog_query, + " AND listed_objects.object_oid IS NOT NULL\n"); + } + + /* + * If no tables were listed, filter for the relevant relation types. If + * tables were given via --table, don't bother filtering by relation type. + * Instead, let the server decide whether a given relation can be + * processed in which case the user will know about it. + */ + if ((objfilter & OBJFILTER_TABLE) == 0) + { + /* + * vacuumdb should generally follow the behavior of the underlying + * VACUUM and ANALYZE commands. If analyze_only is true, process + * regular tables, materialized views, and partitioned tables, just + * like ANALYZE (with no specific target tables) does. Otherwise, + * process only regular tables and materialized views, since VACUUM + * skips partitioned tables when no target tables are specified. + */ + if (vacopts->analyze_only) + appendPQExpBufferStr(&catalog_query, + " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" + CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_MATVIEW) ", " + CppAsString2(RELKIND_PARTITIONED_TABLE) "])\n"); + else + appendPQExpBufferStr(&catalog_query, + " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" + CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_MATVIEW) "])\n"); + } + + /* + * For --min-xid-age and --min-mxid-age, the age of the relation is the + * greatest of the ages of the main relation and its associated TOAST + * table. The commands generated by vacuumdb will also process the TOAST + * table for the relation if necessary, so it does not need to be + * considered separately. + */ + if (vacopts->min_xid_age != 0) + { + appendPQExpBuffer(&catalog_query, + " AND GREATEST(pg_catalog.age(c.relfrozenxid)," + " pg_catalog.age(t.relfrozenxid)) " + " OPERATOR(pg_catalog.>=) '%d'::pg_catalog.int4\n" + " AND c.relfrozenxid OPERATOR(pg_catalog.!=)" + " '0'::pg_catalog.xid\n", + vacopts->min_xid_age); + } + + if (vacopts->min_mxid_age != 0) + { + appendPQExpBuffer(&catalog_query, + " AND GREATEST(pg_catalog.mxid_age(c.relminmxid)," + " pg_catalog.mxid_age(t.relminmxid)) OPERATOR(pg_catalog.>=)" + " '%d'::pg_catalog.int4\n" + " AND c.relminmxid OPERATOR(pg_catalog.!=)" + " '0'::pg_catalog.xid\n", + vacopts->min_mxid_age); + } + + if (vacopts->missing_stats_only) + { + appendPQExpBufferStr(&catalog_query, " AND (\n"); + + /* regular stats */ + appendPQExpBufferStr(&catalog_query, + " EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND a.attgenerated OPERATOR(pg_catalog.<>) " + CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* extended stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" + " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" + " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" + " AND d.stxdinherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* expression indexes */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " JOIN pg_catalog.pg_index i" + " ON i.indexrelid OPERATOR(pg_catalog.=) a.attrelid\n" + " WHERE i.indrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND i.indkey[a.attnum OPERATOR(pg_catalog.-) 1::pg_catalog.int2]" + " OPERATOR(pg_catalog.=) 0::pg_catalog.int2\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* inheritance and regular stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND a.attgenerated OPERATOR(pg_catalog.<>) " + CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" + " AND c.relhassubclass\n" + " AND NOT p.inherited\n" + " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" + " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit))\n"); + + /* inheritance and extended stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" + " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND c.relhassubclass\n" + " AND NOT p.inherited\n" + " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" + " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" + " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" + " AND d.stxdinherit))\n"); + + appendPQExpBufferStr(&catalog_query, " )\n"); + } + + /* + * Execute the catalog query. We use the default search_path for this + * query for consistency with table lookups done elsewhere by the user. + */ + appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;"); + executeCommand(conn, "RESET search_path;", echo); + res = executeQuery(conn, catalog_query.data, echo); + termPQExpBuffer(&catalog_query); + PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo)); + + /* + * Build qualified identifiers for each table, including the column list + * if given. + */ + initPQExpBuffer(&buf); + for (int i = 0; i < PQntuples(res); i++) + { + appendPQExpBufferStr(&buf, + fmtQualifiedIdEnc(PQgetvalue(res, i, 1), + PQgetvalue(res, i, 0), + PQclientEncoding(conn))); + + if (objects_listed && !PQgetisnull(res, i, 2)) + appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2)); + + simple_string_list_append(found_objs, buf.data); + resetPQExpBuffer(&buf); + } + termPQExpBuffer(&buf); + PQclear(res); + + return found_objs; +} + +/* + * Vacuum/analyze all connectable databases. + * + * In analyze-in-stages mode, we process all databases in one stage before + * moving on to the next stage. That ensure minimal stats are available + * quickly everywhere before generating more detailed ones. + */ +void +vacuum_all_databases(ConnParams *cparams, + vacuumingOptions *vacopts, + bool analyze_in_stages, + SimpleStringList *objects, + int concurrentCons, + const char *progname, bool echo, bool quiet) +{ + PGconn *conn; + PGresult *result; + int stage; + int i; + + conn = connectMaintenanceDatabase(cparams, progname, echo); + result = executeQuery(conn, + "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", + echo); + PQfinish(conn); + + if (analyze_in_stages) + { + SimpleStringList **found_objs = NULL; + + if (vacopts->missing_stats_only) + found_objs = palloc0(PQntuples(result) * sizeof(SimpleStringList *)); + + /* + * When analyzing all databases in stages, we analyze them all in the + * fastest stage first, so that initial statistics become available + * for all of them as soon as possible. + * + * This means we establish several times as many connections, but + * that's a secondary consideration. + */ + for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) + { + for (i = 0; i < PQntuples(result); i++) + { + cparams->override_dbname = PQgetvalue(result, i, 0); + + vacuum_one_database(cparams, vacopts, + stage, + objects, + vacopts->missing_stats_only ? &found_objs[i] : NULL, + concurrentCons, + progname, echo, quiet); + } + } + } + else + { + for (i = 0; i < PQntuples(result); i++) + { + cparams->override_dbname = PQgetvalue(result, i, 0); + + vacuum_one_database(cparams, vacopts, + ANALYZE_NO_STAGE, + objects, NULL, + concurrentCons, + progname, echo, quiet); + } + } + + PQclear(result); +} + +/* + * Construct a vacuum/analyze command to run based on the given + * options, in the given string buffer, which may contain previous garbage. + * + * The table name used must be already properly quoted. The command generated + * depends on the server version involved and it is semicolon-terminated. + */ +void +prepare_vacuum_command(PQExpBuffer sql, int serverVersion, + vacuumingOptions *vacopts, const char *table) +{ + const char *paren = " ("; + const char *comma = ", "; + const char *sep = paren; + + resetPQExpBuffer(sql); + + if (vacopts->analyze_only) + { + appendPQExpBufferStr(sql, "ANALYZE"); + + /* parenthesized grammar of ANALYZE is supported since v11 */ + if (serverVersion >= 110000) + { + if (vacopts->skip_locked) + { + /* SKIP_LOCKED is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); + sep = comma; + } + if (vacopts->verbose) + { + appendPQExpBuffer(sql, "%sVERBOSE", sep); + sep = comma; + } + if (vacopts->buffer_usage_limit) + { + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, + vacopts->buffer_usage_limit); + sep = comma; + } + if (sep != paren) + appendPQExpBufferChar(sql, ')'); + } + else + { + if (vacopts->verbose) + appendPQExpBufferStr(sql, " VERBOSE"); + } + } + else + { + appendPQExpBufferStr(sql, "VACUUM"); + + /* parenthesized grammar of VACUUM is supported since v9.0 */ + if (serverVersion >= 90000) + { + if (vacopts->disable_page_skipping) + { + /* DISABLE_PAGE_SKIPPING is supported since v9.6 */ + Assert(serverVersion >= 90600); + appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep); + sep = comma; + } + if (vacopts->no_index_cleanup) + { + /* "INDEX_CLEANUP FALSE" has been supported since v12 */ + Assert(serverVersion >= 120000); + Assert(!vacopts->force_index_cleanup); + appendPQExpBuffer(sql, "%sINDEX_CLEANUP FALSE", sep); + sep = comma; + } + if (vacopts->force_index_cleanup) + { + /* "INDEX_CLEANUP TRUE" has been supported since v12 */ + Assert(serverVersion >= 120000); + Assert(!vacopts->no_index_cleanup); + appendPQExpBuffer(sql, "%sINDEX_CLEANUP TRUE", sep); + sep = comma; + } + if (!vacopts->do_truncate) + { + /* TRUNCATE is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep); + sep = comma; + } + if (!vacopts->process_main) + { + /* PROCESS_MAIN is supported since v16 */ + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep); + sep = comma; + } + if (!vacopts->process_toast) + { + /* PROCESS_TOAST is supported since v14 */ + Assert(serverVersion >= 140000); + appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep); + sep = comma; + } + if (vacopts->skip_database_stats) + { + /* SKIP_DATABASE_STATS is supported since v16 */ + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep); + sep = comma; + } + if (vacopts->skip_locked) + { + /* SKIP_LOCKED is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); + sep = comma; + } + if (vacopts->full) + { + appendPQExpBuffer(sql, "%sFULL", sep); + sep = comma; + } + if (vacopts->freeze) + { + appendPQExpBuffer(sql, "%sFREEZE", sep); + sep = comma; + } + if (vacopts->verbose) + { + appendPQExpBuffer(sql, "%sVERBOSE", sep); + sep = comma; + } + if (vacopts->and_analyze) + { + appendPQExpBuffer(sql, "%sANALYZE", sep); + sep = comma; + } + if (vacopts->parallel_workers >= 0) + { + /* PARALLEL is supported since v13 */ + Assert(serverVersion >= 130000); + appendPQExpBuffer(sql, "%sPARALLEL %d", sep, + vacopts->parallel_workers); + sep = comma; + } + if (vacopts->buffer_usage_limit) + { + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, + vacopts->buffer_usage_limit); + sep = comma; + } + if (sep != paren) + appendPQExpBufferChar(sql, ')'); + } + else + { + if (vacopts->full) + appendPQExpBufferStr(sql, " FULL"); + if (vacopts->freeze) + appendPQExpBufferStr(sql, " FREEZE"); + if (vacopts->verbose) + appendPQExpBufferStr(sql, " VERBOSE"); + if (vacopts->and_analyze) + appendPQExpBufferStr(sql, " ANALYZE"); + } + } + + appendPQExpBuffer(sql, " %s;", table); +} + +/* + * Send a vacuum/analyze command to the server, returning after sending the + * command. + * + * Any errors during command execution are reported to stderr. + */ +void +run_vacuum_command(PGconn *conn, const char *sql, bool echo, + const char *table) +{ + bool status; + + if (echo) + printf("%s\n", sql); + + status = PQsendQuery(conn, sql) == 1; + + if (!status) + { + if (table) + { + pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", + table, PQdb(conn), PQerrorMessage(conn)); + } + else + { + pg_log_error("vacuuming of database \"%s\" failed: %s", + PQdb(conn), PQerrorMessage(conn)); + } + } +} + +/* + * Returns a newly malloc'd version of 'src' with escaped single quotes and + * backslashes. + */ +char * +escape_quotes(const char *src) +{ + char *result = escape_single_quotes_ascii(src); + + if (!result) + pg_fatal("out of memory"); + return result; +} diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h new file mode 100644 index 00000000000..d3f000840fa --- /dev/null +++ b/src/bin/scripts/vacuuming.h @@ -0,0 +1,95 @@ +/*------------------------------------------------------------------------- + * + * vacuuming.h + * Common declarations for vacuuming.c + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/bin/scripts/vacuuming.h + * + *------------------------------------------------------------------------- + */ +#ifndef VACUUMING_H +#define VACUUMING_H + +#include "common.h" +#include "fe_utils/connect_utils.h" +#include "fe_utils/simple_list.h" + +/* For analyze-in-stages mode */ +#define ANALYZE_NO_STAGE -1 +#define ANALYZE_NUM_STAGES 3 + +/* vacuum options controlled by user flags */ +typedef struct vacuumingOptions +{ + bool analyze_only; + bool verbose; + bool and_analyze; + bool full; + bool freeze; + bool disable_page_skipping; + bool skip_locked; + int min_xid_age; + int min_mxid_age; + int parallel_workers; /* >= 0 indicates user specified the + * parallel degree, otherwise -1 */ + bool no_index_cleanup; + bool force_index_cleanup; + bool do_truncate; + bool process_main; + bool process_toast; + bool skip_database_stats; + char *buffer_usage_limit; + bool missing_stats_only; +} vacuumingOptions; + +/* object filter options */ +typedef enum +{ + OBJFILTER_NONE = 0, /* no filter used */ + OBJFILTER_ALL_DBS = (1 << 0), /* -a | --all */ + OBJFILTER_DATABASE = (1 << 1), /* -d | --dbname */ + OBJFILTER_TABLE = (1 << 2), /* -t | --table */ + OBJFILTER_SCHEMA = (1 << 3), /* -n | --schema */ + OBJFILTER_SCHEMA_EXCLUDE = (1 << 4), /* -N | --exclude-schema */ +} VacObjFilter; + +extern VacObjFilter objfilter; + +extern void vacuuming_main(ConnParams *cparams, const char *dbname, + const char *maintenance_db, vacuumingOptions *vacopts, + SimpleStringList *objects, bool analyze_in_stages, + int tbl_count, int concurrentCons, + const char *progname, bool echo, bool quiet); + +extern SimpleStringList *retrieve_objects(PGconn *conn, + vacuumingOptions *vacopts, + SimpleStringList *objects, + bool echo); + +extern void vacuum_one_database(ConnParams *cparams, + vacuumingOptions *vacopts, + int stage, + SimpleStringList *objects, + SimpleStringList **found_objs, + int concurrentCons, + const char *progname, bool echo, bool quiet); + +extern void vacuum_all_databases(ConnParams *cparams, + vacuumingOptions *vacopts, + bool analyze_in_stages, + SimpleStringList *objects, + int concurrentCons, + const char *progname, bool echo, bool quiet); + +extern void prepare_vacuum_command(PQExpBuffer sql, int serverVersion, + vacuumingOptions *vacopts, const char *table); + +extern void run_vacuum_command(PGconn *conn, const char *sql, bool echo, + const char *table); + +extern char *escape_quotes(const char *src); + +#endif /* VACUUMING_H */ -- 2.39.5 --ipmcfwv2cm3fpxaf Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v19-0002-Add-REPACK-command.patch" ^ permalink raw reply [nested|flat] 381+ messages in thread
end of thread, other threads:[~2025-08-30 12:39 UTC | newest] Thread overview: 381+ 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 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 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 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 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 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 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 v29 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 v33 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 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 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 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 v37 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 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 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 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 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 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 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 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]> 2025-08-30 12:39 [PATCH v19 1/6] Split vacuumdb to create vacuuming.c/h Álvaro Herrera <[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