Skip to content

Commit

Permalink
[#24732] YSQL: Refresh materialized view in-place during upgrade
Browse files Browse the repository at this point in the history
Summary:
**Behavior before this change:**
Non-concurrent refresh of a materialized view involves generating the new data in a new DocDB table, and then swapping the tables by replacing the `relfinenode` info of the matview. This involves 16 RPCs to yb-master (pg catalog) with 73 pgsql_write_batches. All of these are not allowed during the ysql major upgrade.
Concurrent refresh of a supported materialized view involves generating the new data in temp table, followed by doing a merge of the new data with the old data already in the matview using another temp table.
This involves 24 RPC calls to yb-master (pg catalog) with 241 pgsql_write_batches. However 23 of these RPCs are related to the temp tables (force_catalog_modifications set) which are allowed during ysql major upgrade. The only other RPC is related to modifying the `relispopulated` field.

**Behavior with this change:**
During ysql major upgrade changes to the `relispopulated` field is blocked. This is not an issue with concurrent refresh, since those are always blocked.
The non-concurrent refresh of matview now generates the new data in a temporary table, and use the more expensive method of running `DELETE FROM <matview>` followed by a `INSERT INTO <matview> SELECT * FROM <temp_table>`.
The advantage of this method is that all pg catalog modifications are only done to the temp table, which is allowed during the upgrade.
The performance tradeoff is acceptable, since we only expect small matviews to be refreshed frequently, hences needed during the upgrade, whereas the bigger ones can wait till after the upgrade to be refreshed.

This behavior is enabled by `yb_refresh_matview_in_place` or `yb_major_version_upgrade_compatibility>0`.

Concurrent refresh has not been modified, since it only creates temporary tables, and does not involve any other catalog modifications.

Fixes #24732
Jira: DB-13813

Test Plan: YsqlMajorUpgradeMatviewTest.TestMatView

Reviewers: telgersma, smishra, myang

Reviewed By: telgersma, myang

Subscribers: svc_phabricator, ybase, yql

Differential Revision: https://phorge.dev.yugabyte.com/D41504
  • Loading branch information
hari90 committed Jan 31, 2025
1 parent 158cb9b commit 2e5e720
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 23 deletions.
3 changes: 2 additions & 1 deletion src/postgres/src/backend/commands/createas.c
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
* going to fill it; otherwise, no change needed.
*/
if (is_matview && !into->skipData)
SetMatViewPopulatedState(intoRelationDesc, true);
SetMatViewPopulatedState(intoRelationDesc, true,
false /* yb_in_place_refresh */ );

/*
* Fill private fields of myState for use by later routines
Expand Down
96 changes: 90 additions & 6 deletions src/postgres/src/backend/commands/matview.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,18 @@ static bool is_usable_unique_index(Relation indexRel);
static void OpenMatViewIncrementalMaintenance(void);
static void CloseMatViewIncrementalMaintenance(void);

static bool yb_needs_in_place_refresh();
static void yb_refresh_in_place_update(Relation matviewRel, Oid tempOid);

/*
* SetMatViewPopulatedState
* Mark a materialized view as populated, or not.
*
* NOTE: caller must be holding an appropriate lock on the relation.
*/
void
SetMatViewPopulatedState(Relation relation, bool newstate)
SetMatViewPopulatedState(Relation relation, bool newstate,
bool yb_in_place_refresh)
{
Relation pgrel;
HeapTuple tuple;
Expand All @@ -105,6 +109,17 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
elog(ERROR, "cache lookup failed for relation %u",
RelationGetRelid(relation));

if (yb_in_place_refresh)
{
if (((Form_pg_class) GETSTRUCT(tuple))->relispopulated != newstate)
elog(ERROR, "Cannot change the populated state of a materialized "
"view when in place refresh is enabled");

heap_freetuple(tuple);
table_close(pgrel, RowExclusiveLock);
return;
}

((Form_pg_class) GETSTRUCT(tuple))->relispopulated = newstate;

CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
Expand Down Expand Up @@ -160,6 +175,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
int save_sec_context;
int save_nestlevel;
ObjectAddress address;
bool yb_in_place_refresh = yb_needs_in_place_refresh();

/* Determine strength of lock needed. */
concurrent = stmt->concurrent;
Expand Down Expand Up @@ -283,10 +299,10 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
* Tentatively mark the matview as populated or not (this will roll back
* if we fail later).
*/
SetMatViewPopulatedState(matviewRel, !stmt->skipData);
SetMatViewPopulatedState(matviewRel, !stmt->skipData, yb_in_place_refresh);

/* Concurrent refresh builds new data in temp tablespace, and does diff. */
if (concurrent)
if (concurrent || yb_in_place_refresh)
{
tableSpace = GetDefaultTablespace(RELPERSISTENCE_TEMP, false);
relpersistence = RELPERSISTENCE_TEMP;
Expand All @@ -297,6 +313,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
relpersistence = matviewRel->rd_rel->relpersistence;
}

/*
* Required to allow the creation for the temp table since we directly
* call make_new_heap instead of going through DefineRelation.
*/
if (yb_in_place_refresh)
YBCDdlEnableForceCatalogModification();

/*
* Create the transient table that will receive the regenerated data. Lock
* it against access by any other process until commit (by which time it
Expand All @@ -314,14 +337,17 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
processed = refresh_matview_datafill(dest, dataQuery, queryString);

/* Make the matview match the newly generated data. */
if (concurrent)
if (concurrent || yb_in_place_refresh)
{
int old_depth = matview_maintenance_depth;

PG_TRY();
{
refresh_by_match_merge(matviewOid, OIDNewHeap, relowner,
save_sec_context);
if (!concurrent)
yb_refresh_in_place_update(matviewRel, OIDNewHeap);
else
refresh_by_match_merge(matviewOid, OIDNewHeap, relowner,
save_sec_context);
}
PG_CATCH();
{
Expand Down Expand Up @@ -1050,3 +1076,61 @@ CloseMatViewIncrementalMaintenance(void)
matview_maintenance_depth--;
Assert(matview_maintenance_depth >= 0);
}

static bool
yb_needs_in_place_refresh()
{
return yb_refresh_matview_in_place ||
yb_major_version_upgrade_compatibility > 0;
}

/*
* yb_refresh_in_place_update
* Refresh the matview by using the same heap/DocDB table. Deletes all rows from
* the table and inserts the data from the temp table.
*/
static void
yb_refresh_in_place_update(Relation matviewRel, Oid tempOid)
{
Assert(IsYugaByteEnabled());

StringInfoData querybuf;
Relation tempRel;
char *matviewname;
char *tempname;

initStringInfo(&querybuf);
matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)),
RelationGetRelationName(matviewRel));
tempRel = table_open(tempOid, NoLock);
tempname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(tempRel)),
RelationGetRelationName(tempRel));

if (SPI_connect() != SPI_OK_CONNECT)
elog(ERROR, "SPI_connect failed");

OpenMatViewIncrementalMaintenance();

appendStringInfo(&querybuf, "DELETE FROM %s", matviewname);

if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE)
elog(ERROR, "SPI_exec failed: %s", querybuf.data);

resetStringInfo(&querybuf);
appendStringInfo(&querybuf, "INSERT INTO %s SELECT * FROM %s", matviewname,
tempname);

if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT)
elog(ERROR, "SPI_exec failed: %s", querybuf.data);

CloseMatViewIncrementalMaintenance();
table_close(tempRel, NoLock);

resetStringInfo(&querybuf);
appendStringInfo(&querybuf, "DROP TABLE %s", tempname);
if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY)
elog(ERROR, "SPI_exec failed: %s", querybuf.data);

if (SPI_finish() != SPI_OK_FINISH)
elog(ERROR, "SPI_finish failed");
}
23 changes: 23 additions & 0 deletions src/postgres/src/backend/utils/misc/guc.c
Original file line number Diff line number Diff line change
Expand Up @@ -3142,6 +3142,17 @@ static struct config_bool ConfigureNamesBool[] =
NULL, NULL, NULL
},

{
{"yb_refresh_matview_in_place", PGC_USERSET, CUSTOM_OPTIONS,
gettext_noop("Refresh materialized views in place."),
NULL,
GUC_NOT_IN_SAMPLE
},
&yb_refresh_matview_in_place,
false,
NULL, NULL, NULL
},

/* End-of-list marker */
{
{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
Expand Down Expand Up @@ -4995,6 +5006,18 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},

{
{"yb_major_version_upgrade_compatibility", PGC_SIGHUP, CUSTOM_OPTIONS,
gettext_noop("The compatibility level to use during a YSQL Major version upgrade. "
"Allowed values are 0 and 11."),
NULL,
GUC_NOT_IN_SAMPLE
},
&yb_major_version_upgrade_compatibility,
0, 0, INT_MAX,
NULL, NULL, NULL
},

/* End-of-list marker */
{
{NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
Expand Down
4 changes: 2 additions & 2 deletions src/postgres/src/include/commands/matview.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
#include "tcop/dest.h"
#include "utils/relcache.h"


extern void SetMatViewPopulatedState(Relation relation, bool newstate);
extern void SetMatViewPopulatedState(Relation relation, bool newstate,
bool yb_in_place_refresh);

extern ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
ParamListInfo params, QueryCompletion *qc);
Expand Down
1 change: 1 addition & 0 deletions src/yb/integration-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ ADD_YB_TEST(upgrade-tests/pg15_upgrade-test)
ADD_YB_TEST(upgrade-tests/pg15_upgrade_pgregress-test)
ADD_YB_TEST(upgrade-tests/ysql_major_upgrade_rpcs-test)
ADD_YB_TEST(upgrade-tests/ysql_major_upgrade_ddl_blocking-test)
ADD_YB_TEST(upgrade-tests/ysql_major_upgrade_matview-test)

set(YB_TEST_LINK_LIBS_SAVED ${YB_TEST_LINK_LIBS})
set(YB_TEST_LINK_LIBS ${YB_TEST_LINK_LIBS} cassandra)
Expand Down
13 changes: 12 additions & 1 deletion src/yb/integration-tests/upgrade-tests/upgrade_test_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,12 @@ Status UpgradeTestBase::StartClusterInOldVersion(const ExternalMiniClusterOption
current_version_info_.ysql_major_version();

if (IsYsqlMajorVersionUpgrade()) {
// YB_TODO: Remove when support for expression pushdown in mixed mode is implemented.
// TODO: Remove when support for expression pushdown in mixed mode is implemented.
RETURN_NOT_OK(cluster_->AddAndSetExtraFlag("ysql_yb_enable_expression_pushdown", "false"));

// TODO: Enable after flag is backported to older version.
// RETURN_NOT_OK(cluster_->AddAndSetExtraFlag("ysql_yb_major_version_upgrade_compatibility",
// "11"));
}

return Status::OK();
Expand Down Expand Up @@ -508,6 +512,13 @@ Status UpgradeTestBase::FinalizeYsqlMajorCatalogUpgrade() {
return StatusFromPB(resp.error().status());
}

if (IsYsqlMajorVersionUpgrade()) {
// TODO: Remove when support for expression pushdown in mixed mode is implemented.
RETURN_NOT_OK(cluster_->AddAndSetExtraFlag("ysql_yb_enable_expression_pushdown", "true"));

RETURN_NOT_OK(cluster_->AddAndSetExtraFlag("ysql_yb_major_version_upgrade_compatibility", "0"));
}

return Status::OK();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,10 @@ class YsqlMajorUpgradeDdlBlockingTest : public Pg15UpgradeTestBase {
if (!error_expected) {
RETURN_NOT_OK(status);
} else {
if (upgrade_state_ == UpgradeState::kAfterUpgrade) {
// Depending on the cleanup state we may get different errors after the upgrade.
SCHECK(
!status.ok() &&
(status.message().ToString().find(kExpectedDdlError) != std::string::npos ||
status.message().ToString().find("unknown_table_name") != std::string::npos),
IllegalState, "Unexpected status: ", status.ToString());
} else {
SCHECK(
!status.ok() &&
status.message().ToString().find(kExpectedDdlError) != std::string::npos,
IllegalState, "Unexpected status: ", status.ToString());
}
SCHECK(
!status.ok() &&
status.message().ToString().find(kExpectedDdlError) != std::string::npos,
IllegalState, "Unexpected status: ", status.ToString());
}
}
return Status::OK();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) YugabyteDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations
// under the License.
//

#include "yb/integration-tests/upgrade-tests/pg15_upgrade_test_base.h"

#include "yb/yql/pgwrapper/libpq_utils.h"

namespace yb {

static constexpr auto kTableName = "tbl1";
static constexpr auto kNormalMatviewName = "mv1";
static constexpr auto kIndexedMatViewName = "mv2";

class YsqlMajorUpgradeMatviewTest : public Pg15UpgradeTestBase {
public:
void SetUp() override {
Pg15UpgradeTestBase::SetUp();
if (Test::IsSkipped()) {
return;
}

auto conn = ASSERT_RESULT(CreateConnToTs(std::nullopt));
ASSERT_OK(conn.ExecuteFormat("CREATE TABLE $0(a int, b int)", kTableName));
ASSERT_OK(conn.ExecuteFormat(
"CREATE MATERIALIZED VIEW $0 AS SELECT a FROM $1", kNormalMatviewName, kTableName));
ASSERT_OK(conn.ExecuteFormat(
"CREATE MATERIALIZED VIEW $0 AS SELECT a FROM $1", kIndexedMatViewName, kTableName));
ASSERT_OK(conn.ExecuteFormat("CREATE UNIQUE INDEX idx1 ON $0(a)", kIndexedMatViewName));

ASSERT_OK(InsertDataRefreshAndValidate());
}

Status InsertDataRefreshAndValidate(
std::optional<size_t> ts_id = std::nullopt, int row_count = 10) {
auto conn = VERIFY_RESULT(CreateConnToTs(ts_id));
RETURN_NOT_OK(InsertData(conn, row_count));
return RefreshMatviewsAndValidate(conn);
}

Status InsertData(pgwrapper::PGConn& conn, int row_count) {
for (int i = 0; i < row_count; ++i) {
RETURN_NOT_OK(conn.ExecuteFormat("INSERT INTO $0 VALUES ($1, $1)", kTableName, ++num_rows_));
}
return Status::OK();
}

Status RefreshMatviewsAndValidate(pgwrapper::PGConn& conn) {
RETURN_NOT_OK(conn.ExecuteFormat("REFRESH MATERIALIZED VIEW $0", kNormalMatviewName));
RETURN_NOT_OK(
conn.ExecuteFormat("REFRESH MATERIALIZED VIEW CONCURRENTLY $0", kIndexedMatViewName));

auto table_data = VERIFY_RESULT(conn.FetchRows<int>("SELECT a FROM tbl1 ORDER BY a"));
LOG(INFO) << "Table data: " << yb::ToString(table_data);

auto normal_matview_data = VERIFY_RESULT(conn.FetchRows<int>("SELECT a FROM mv1 ORDER BY a"));
SCHECK_EQ(
table_data, normal_matview_data, IllegalState,
Format("Normal matview data mismatch: $0", yb::ToString(normal_matview_data)));

auto indexes_matview_data = VERIFY_RESULT(conn.FetchRows<int>("SELECT a FROM mv2 ORDER BY a"));
SCHECK_EQ(
table_data, indexes_matview_data, IllegalState,
Format("Indexed matview data mismatch: $0", yb::ToString(indexes_matview_data)));

return Status::OK();
}

private:
uint32 num_rows_ = 0;
};

TEST_F(YsqlMajorUpgradeMatviewTest, TestMatView) {
ASSERT_OK(UpgradeClusterToMixedMode());

ASSERT_NOK(InsertDataRefreshAndValidate(kMixedModeTserverPg11));

// TODO: Pending backport
ASSERT_OK(cluster_->SetFlag(
cluster_->tablet_server(kMixedModeTserverPg15), "ysql_yb_major_version_upgrade_compatibility",
"11"));

ASSERT_OK(InsertDataRefreshAndValidate(kMixedModeTserverPg15));

ASSERT_OK(FinalizeUpgradeFromMixedMode());

ASSERT_OK(InsertDataRefreshAndValidate());
}

} // namespace yb
4 changes: 4 additions & 0 deletions src/yb/yql/pggate/util/ybc_guc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@ bool yb_allow_block_based_sampling_algorithm = true;
// TODO(#24089): Once code duplication between yb_guc and ybc_util is removed, we should be able
// to use YB_SAMPLING_ALGORITHM_BLOCK_BASED_SAMPLING instead of 1 and do it in one place.
int32_t yb_sampling_algorithm = 1 /* YB_SAMPLING_ALGORITHM_BLOCK_BASED_SAMPLING */;

bool yb_refresh_matview_in_place = false;

int yb_major_version_upgrade_compatibility = 0;
4 changes: 4 additions & 0 deletions src/yb/yql/pggate/util/ybc_guc.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ extern int yb_read_after_commit_visibility;

extern bool yb_allow_block_based_sampling_algorithm;

extern bool yb_refresh_matview_in_place;

extern int yb_major_version_upgrade_compatibility;

// Should be in sync with YsqlSamplingAlgorithm protobuf.
typedef enum {
YB_SAMPLING_ALGORITHM_FULL_TABLE_SCAN = 0,
Expand Down
Loading

0 comments on commit 2e5e720

Please sign in to comment.