diff --git a/java/yb-pgsql/src/test/java/org/yb/pgsql/TestPgRegressUpdateOptimized.java b/java/yb-pgsql/src/test/java/org/yb/pgsql/TestPgRegressUpdateOptimized.java new file mode 100644 index 000000000000..781e7304cd54 --- /dev/null +++ b/java/yb-pgsql/src/test/java/org/yb/pgsql/TestPgRegressUpdateOptimized.java @@ -0,0 +1,39 @@ +// 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. +// +package org.yb.pgsql; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.yb.YBTestRunner; + +@RunWith(value=YBTestRunner.class) +public class TestPgRegressUpdateOptimized extends BasePgRegressTest { + @Override + public int getTestMethodTimeoutSec() { + return 1800; + } + + @Override + protected Map getTServerFlags() { + Map flagMap = super.getTServerFlags(); + flagMap.put("ysql_skip_row_lock_for_update", "false"); + return flagMap; + } + + @Test + public void schedule() throws Exception { + runPgRegressTest("yb_update_optimized_schedule"); + } +} diff --git a/src/postgres/src/backend/commands/copyfrom.c b/src/postgres/src/backend/commands/copyfrom.c index a71e6f6250be..02dab64ce480 100644 --- a/src/postgres/src/backend/commands/copyfrom.c +++ b/src/postgres/src/backend/commands/copyfrom.c @@ -353,7 +353,7 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo, recheckIndexes = ExecInsertIndexTuples(resultRelInfo, buffer->slots[i], estate, false, false, - NULL, NIL, NIL /* no_update_index_list */); + NULL, NIL); ExecARInsertTriggers(estate, resultRelInfo, slots[i], recheckIndexes, cstate->transition_capture); @@ -1234,7 +1234,6 @@ CopyFrom(CopyFromState cstate) false, false, NULL, - NIL, NIL); } else if (resultRelInfo->ri_FdwRoutine != NULL) @@ -1270,8 +1269,7 @@ CopyFrom(CopyFromState cstate) false, false, NULL, - NIL, - NIL /* no_update_index_list */); + NIL); } /* AFTER ROW INSERT Triggers */ diff --git a/src/postgres/src/backend/commands/trigger.c b/src/postgres/src/backend/commands/trigger.c index 21bc0774c356..6611ba1932fa 100644 --- a/src/postgres/src/backend/commands/trigger.c +++ b/src/postgres/src/backend/commands/trigger.c @@ -6423,7 +6423,8 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, /* Update or delete on trigger's PK table */ if (!RI_FKey_pk_upd_check_required(trigger, rel, - oldslot, newslot)) + oldslot, newslot, + &estate->yb_skip_entities)) { /* skip queuing this event */ continue; @@ -6445,7 +6446,8 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || !RI_FKey_fk_upd_check_required(trigger, rel, - oldslot, newslot)) + oldslot, newslot, + &estate->yb_skip_entities)) { /* skip queuing this event */ continue; diff --git a/src/postgres/src/backend/executor/Makefile b/src/postgres/src/backend/executor/Makefile index 97d0f10626ce..330ae2dd5037 100644 --- a/src/postgres/src/backend/executor/Makefile +++ b/src/postgres/src/backend/executor/Makefile @@ -17,6 +17,7 @@ OBJS = \ nodeYbBitmapIndexscan.o \ nodeYbBitmapTablescan.o \ nodeYbSeqscan.o \ + ybOptimizeModifyTable.o \ ybcExpr.o \ ybcFunction.o \ ybcModifyTable.o \ diff --git a/src/postgres/src/backend/executor/execIndexing.c b/src/postgres/src/backend/executor/execIndexing.c index 6169d07b7fab..4101c6a34d17 100644 --- a/src/postgres/src/backend/executor/execIndexing.c +++ b/src/postgres/src/backend/executor/execIndexing.c @@ -291,8 +291,7 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, bool update, bool noDupErr, bool *specConflict, - List *arbiterIndexes, - List *no_update_index_list) + List *arbiterIndexes) { ItemPointer tupleid = &slot->tts_tid; List *result = NIL; @@ -348,9 +347,10 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, * For an update command check if we need to skip index. For that purpose, * we check if the relid of the index is part of the skip list. */ - if (indexRelation == NULL || (no_update_index_list && - list_member_oid(no_update_index_list, RelationGetRelid(indexRelation)))) - continue; + if (indexRelation == NULL || + list_member_oid(estate->yb_skip_entities.index_list, + RelationGetRelid(indexRelation))) + continue; indexInfo = indexInfoArray[i]; Assert(indexInfo->ii_ReadyForInserts == @@ -433,11 +433,18 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, * There's definitely going to be an index_insert() call for this * index. If we're being called as part of an UPDATE statement, * consider if the 'indexUnchanged' = true hint should be passed. + * + * YB Note: In case of a Yugabyte relation, we have already computed if + * the index is unchanged (partly at planning time, and partly during + * execution in ExecUpdate). Further, the result of this computation is + * not just a hint, but is enforced by skipping RPCs to the storage + * layer. Hence this variable is not relevant for Yugabyte relations and + * will always evaluate to false. */ - indexUnchanged = update && index_unchanged_by_update(resultRelInfo, - estate, - indexInfo, - indexRelation); + indexUnchanged = isYBRelation && update && index_unchanged_by_update(resultRelInfo, + estate, + indexInfo, + indexRelation); satisfiesConstraint = index_insert(indexRelation, /* index relation */ @@ -525,17 +532,6 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, */ void ExecDeleteIndexTuples(ResultRelInfo *resultRelInfo, Datum ybctid, HeapTuple tuple, EState *estate) -{ - ExecDeleteIndexTuplesOptimized(resultRelInfo, ybctid, tuple, estate, - NIL /* no_update_index_list */); -} - -void -ExecDeleteIndexTuplesOptimized(ResultRelInfo *resultRelInfo, - Datum ybctid, - HeapTuple tuple, - EState *estate, - List *no_update_index_list) { int i; int numIndices; @@ -584,9 +580,10 @@ ExecDeleteIndexTuplesOptimized(ResultRelInfo *resultRelInfo, * For an update command check if we need to skip index. * For that purpose, we check if the relid of the index is part of the skip list. */ - if (indexRelation == NULL || (no_update_index_list && - list_member_oid(no_update_index_list, RelationGetRelid(indexRelation)))) - continue; + if (indexRelation == NULL || + list_member_oid(estate->yb_skip_entities.index_list, + RelationGetRelid(indexRelation))) + continue; /* * No need to update YugaByte primary key which is intrinic part of diff --git a/src/postgres/src/backend/executor/execReplication.c b/src/postgres/src/backend/executor/execReplication.c index 6aa95276189b..6587b3d34277 100644 --- a/src/postgres/src/backend/executor/execReplication.c +++ b/src/postgres/src/backend/executor/execReplication.c @@ -447,7 +447,7 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo, if (resultRelInfo->ri_NumIndices > 0) recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, false, false, - NULL, NIL, NIL /* no_update_index_list */); + NULL, NIL); /* AFTER ROW INSERT Triggers */ ExecARInsertTriggers(estate, resultRelInfo, slot, @@ -515,7 +515,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo, if (resultRelInfo->ri_NumIndices > 0 && update_indexes) recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, true, false, - NULL, NIL, NIL /* no_update_index_list */); + NULL, NIL); /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(estate, resultRelInfo, diff --git a/src/postgres/src/backend/executor/execUtils.c b/src/postgres/src/backend/executor/execUtils.c index aff7047dd99f..946829366782 100644 --- a/src/postgres/src/backend/executor/execUtils.c +++ b/src/postgres/src/backend/executor/execUtils.c @@ -165,6 +165,8 @@ CreateExecutorState(void) estate->es_jit_flags = 0; estate->es_jit = NULL; + NodeSetTag(&estate->yb_skip_entities, T_YbSkippableEntities); + /* * Return the executor state structure */ diff --git a/src/postgres/src/backend/executor/nodeModifyTable.c b/src/postgres/src/backend/executor/nodeModifyTable.c index 3d25411cc9c0..a21c4fa005d5 100644 --- a/src/postgres/src/backend/executor/nodeModifyTable.c +++ b/src/postgres/src/backend/executor/nodeModifyTable.c @@ -84,6 +84,7 @@ #include "access/yb_scan.h" #include "catalog/pg_database.h" #include "executor/ybcModifyTable.h" +#include "executor/ybOptimizeModifyTable.h" #include "optimizer/ybcplan.h" #include "parser/parsetree.h" #include "utils/typcache.h" @@ -1172,8 +1173,7 @@ ExecInsert(ModifyTableContext *context, /* insert index entries for tuple */ recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, true, true, - &specConflict, arbiterIndexes, - NIL /* no_update_index_list */); + &specConflict, arbiterIndexes); } else { @@ -1196,8 +1196,7 @@ ExecInsert(ModifyTableContext *context, recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, false, true, &specConflict, - arbiterIndexes, - NIL /* no_update_index_list */); + arbiterIndexes); /* adjust the tuple's state accordingly */ table_tuple_complete_speculative(resultRelationDesc, slot, @@ -1237,8 +1236,7 @@ ExecInsert(ModifyTableContext *context, /* insert index entries for tuple */ if (YBCRelInfoHasSecondaryIndices(resultRelInfo)) recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, false, true, - NULL, NIL, - NIL /* no_update_index_list */); + NULL, NIL); MemoryContextSwitchTo(oldContext); } else @@ -1251,8 +1249,7 @@ ExecInsert(ModifyTableContext *context, if (resultRelInfo->ri_NumIndices > 0) recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, estate, false, - false, NULL, NIL, - NIL /* no_update_index_list */); + false, NULL, NIL); } } } @@ -1867,83 +1864,6 @@ ldelete:; return NULL; } -/* ---------------------------------------------------------------- - * YBEqualDatums - * - * Function compares values of lhs and rhs datums with respect to value type and collation. - * - * Returns true in case value of lhs and rhs datums match. - * ---------------------------------------------------------------- - */ -static bool -YBEqualDatums(Datum lhs, Datum rhs, Oid atttypid, Oid collation) -{ - LOCAL_FCINFO(locfcinfo, 2); - TypeCacheEntry *typentry = lookup_type_cache(atttypid, TYPECACHE_CMP_PROC_FINFO); - if (!OidIsValid(typentry->cmp_proc_finfo.fn_oid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_FUNCTION), - errmsg("could not identify a comparison function for type %s", - format_type_be(typentry->type_id)))); - - InitFunctionCallInfoData(*locfcinfo, &typentry->cmp_proc_finfo, 2, collation, NULL, NULL); - locfcinfo->args[0].value = lhs; - locfcinfo->args[0].isnull = false; - locfcinfo->args[1].value = rhs; - locfcinfo->args[1].isnull = false; - return DatumGetInt32(FunctionCallInvoke(locfcinfo)) == 0; -} - -/* ---------------------------------------------------------------- - * YBBuildExtraUpdatedCols - * - * Function compares attribute value in oldtuple and newslot for attributes which are not in the - * updatedCols set. Returns set of changed attributes or NULL. - * ---------------------------------------------------------------- - */ -static Bitmapset* -YBBuildExtraUpdatedCols(Relation rel, - HeapTuple oldtuple, - TupleTableSlot *newslot, - Bitmapset *updatedCols) -{ - if (bms_is_member(InvalidAttrNumber, updatedCols)) - /* No extra work required in case the whore row is changed */ - return NULL; - - Bitmapset *result = NULL; - AttrNumber firstLowInvalidAttributeNumber = YBGetFirstLowInvalidAttributeNumber(rel); - TupleDesc tupleDesc = RelationGetDescr(rel); - for (int idx = 0; idx < tupleDesc->natts; ++idx) - { - FormData_pg_attribute *att_desc = TupleDescAttr(tupleDesc, idx); - - AttrNumber attnum = att_desc->attnum; - - /* Skip virtual (system) and dropped columns */ - if (!IsRealYBColumn(rel, attnum)) - continue; - - int bms_idx = attnum - firstLowInvalidAttributeNumber; - if (bms_is_member(bms_idx, updatedCols)) - continue; - - bool old_is_null = false; - bool new_is_null = false; - Datum old_value = heap_getattr(oldtuple, attnum, tupleDesc, &old_is_null); - Datum new_value = slot_getattr(newslot, attnum, &new_is_null); - if (old_is_null != new_is_null || - (!new_is_null && !YBEqualDatums(old_value, - new_value, - att_desc->atttypid, - att_desc->attcollation))) - { - result = bms_add_member(result, bms_idx); - } - } - return result; -} - /* * ExecCrossPartitionUpdate --- Move an updated tuple to another partition. * @@ -2347,6 +2267,7 @@ lreplace:; /* YB_TODO(arpan): Deduplicate code between YBExecUpdateAct and ExecUpdateAct */ /* YB_TODO(review) Revisit later. */ +/* YB_TODO(kramanathan): Evaluate use of ExecFetchSlotHeapTuple */ static bool YBExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, @@ -2368,7 +2289,7 @@ YBExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, yb_lreplace:; /* ensure slot is independent, consider e.g. EPQ */ - ExecMaterializeSlot(slot); + HeapTuple tuple = ExecFetchSlotHeapTuple(slot, true /* materialize*/, NULL); /* * Check the constraints of the tuple. @@ -2469,22 +2390,16 @@ yb_lreplace:; bool beforeRowUpdateTriggerFired = resultRelInfo->ri_TrigDesc && resultRelInfo->ri_TrigDesc->trig_update_before_row; - Bitmapset *actualUpdatedCols = rte->updatedCols; - Bitmapset *extraUpdatedCols = NULL; - if (beforeRowUpdateTriggerFired) - { - /* trigger might have changed tuple */ - extraUpdatedCols = YBBuildExtraUpdatedCols(resultRelationDesc, oldtuple, - slot, rte->updatedCols); - if (extraUpdatedCols) - { - extraUpdatedCols = - bms_add_members(extraUpdatedCols, rte->updatedCols); - actualUpdatedCols = extraUpdatedCols; - } - } - Bitmapset *extraGeneratedCols = NULL; + /* + * A bitmapset of columns that have been marked as being updated at + * planning time. This set needs to be updated below if either: + * - The update optimization comparing new and old values to identify + * actually modified columns is enabled. + * - Before row triggers were fired. + */ + Bitmapset *cols_marked_for_update = bms_copy(rte->updatedCols); + if (resultRelInfo->ri_NumGeneratedNeeded > 0) { /* @@ -2494,12 +2409,40 @@ yb_lreplace:; Bitmapset *generatedCols = ExecGetExtraUpdatedCols(resultRelInfo, estate); Assert(!bms_is_empty(generatedCols)); - extraGeneratedCols = bms_union(actualUpdatedCols, generatedCols); - actualUpdatedCols = extraGeneratedCols; + cols_marked_for_update = bms_union(cols_marked_for_update, generatedCols); + } + + ModifyTable *plan = (ModifyTable *) context->mtstate->ps.plan; + YbCopySkippableEntities(&estate->yb_skip_entities, + plan->yb_skip_entities); + + /* + * If an update is a "single row transaction", then we have already + * confirmed at planning time that it has no secondary indexes or + * triggers or foreign key constraints. Such an update does not + * benefit from optimizations that skip constraint checking or index + * updates. + * While it may seem that a single row, distributed transaction can be + * transformed into a single row, non-distributed transaction, this is + * not the case. It is likely that the row to be updated has been read + * from the storage layer already, thus violating the non-distributed + * transaction semantics. + */ + if (!estate->yb_es_is_single_row_modify_txn) + { + YbComputeModifiedColumnsAndSkippableEntities( + plan, resultRelInfo, estate, oldtuple, tuple, + &cols_marked_for_update, beforeRowUpdateTriggerFired); } - Bitmapset *primary_key_bms = YBGetTablePrimaryKeyBms(resultRelationDesc); - bool is_pk_updated = bms_overlap(primary_key_bms, actualUpdatedCols); + /* + * Irrespective of whether the optimization is enabled or not, we have + * to check if the primary key is updated. It could be that the columns + * making up the primary key are not a part of the target list but are + * updated by a before row trigger. + */ + bool is_pk_updated = YbIsPrimaryKeyUpdated(resultRelationDesc, + cols_marked_for_update); /* * TODO(alex): It probably makes more sense to pass a @@ -2507,7 +2450,6 @@ yb_lreplace:; * that it can have tuple materialized already. */ - ModifyTable *plan = (ModifyTable *) context->mtstate->ps.plan; if (is_pk_updated) { YBCExecuteUpdateReplace(resultRelationDesc, context->planSlot, slot, estate); @@ -2519,10 +2461,8 @@ yb_lreplace:; context->mtstate->yb_fetch_target_tuple, estate->yb_es_is_single_row_modify_txn ? YB_SINGLE_SHARD_TRANSACTION : YB_TRANSACTIONAL, - actualUpdatedCols, canSetTag); + cols_marked_for_update, canSetTag); - bms_free(extraGeneratedCols); - bms_free(extraUpdatedCols); return row_found; } @@ -2552,19 +2492,15 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, mtstate->yb_fetch_target_tuple) { Datum ybctid = YBCGetYBTupleIdFromSlot(context->planSlot); - List *no_update_index_list = - ((ModifyTable *) mtstate->ps.plan)->no_update_index_list; /* Delete index entries of the old tuple */ - ExecDeleteIndexTuplesOptimized(resultRelInfo, ybctid, oldtuple, - context->estate, - no_update_index_list); + ExecDeleteIndexTuples(resultRelInfo, ybctid, oldtuple, + context->estate); /* Insert new index entries for tuple */ recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, context->estate, false, true, - NULL, NIL, - no_update_index_list); + NULL, NIL); } } else @@ -2573,8 +2509,7 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, recheckIndexes = ExecInsertIndexTuples(resultRelInfo, slot, context->estate, true, false, - NULL, NIL, - NIL /* no_update_index_list */); + NULL, NIL); } if (!((ModifyTable *) mtstate->ps.plan)->no_row_trigger) @@ -2946,6 +2881,8 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ExecUpdateEpilogue(context, &updateCxt, resultRelInfo, tupleid, oldtuple, slot, recheckIndexes); + YbClearSkippableEntities(&estate->yb_skip_entities); + list_free(recheckIndexes); /* Process RETURNING if present */ diff --git a/src/postgres/src/backend/executor/ybOptimizeModifyTable.c b/src/postgres/src/backend/executor/ybOptimizeModifyTable.c new file mode 100644 index 000000000000..a88ca9b85a5f --- /dev/null +++ b/src/postgres/src/backend/executor/ybOptimizeModifyTable.c @@ -0,0 +1,516 @@ +/*------------------------------------------------------------------------- + * + * ybOptimizeModifyTable.c + * Support routines for optimizing modify table operations in YugabyteDB + * relations. + * + * 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. + * + * + * IDENTIFICATION + * src/backend/executor/ybOptimizeModifyTable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/sysattr.h" +#include "catalog/catalog.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_trigger.h" +#include "executor/executor.h" +#include "executor/ybOptimizeModifyTable.h" +#include "nodes/ybbitmatrix.h" +#include "optimizer/ybcplan.h" +#include "parser/parsetree.h" +#include "pg_yb_utils.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/syscache.h" + +static inline int +YbGetNextEntityFromBitMatrix(const YbBitMatrix *matrix, int field_idx, + int prev_entity_idx) +{ + return YbBitMatrixNextMemberInRow(matrix, field_idx, prev_entity_idx); +} + +static inline int +YbGetNextFieldFromBitMatrix(const YbBitMatrix *matrix, int entity_idx, + int prev_field_idx) +{ + return YbBitMatrixNextMemberInColumn(matrix, entity_idx, prev_field_idx); +} + +static void +YbLogOptimizationSummary(const struct YbUpdateEntity *entity_list, + Bitmapset *modified_entities, int nentities) +{ + if (log_min_messages >= DEBUG1) + return; + + for (int i = 0; i < nentities; i++) + { + if (bms_is_member(i, modified_entities)) + elog(DEBUG2, "Index/constraint with oid %d requires an update", + entity_list[i].oid); + else + elog(DEBUG2, "Index/constraint with oid %d is unmodified", + entity_list[i].oid); + } +} + +static void +YbLogColumnList(Relation rel, const Bitmapset *cols, const char *message) +{ + const int ncols = bms_num_members(cols); + if (!ncols) + { + elog(DEBUG2, "No cols in category: %s", message); + return; + } + + char *col_str = (char *) palloc0(10 * ncols * sizeof(char)); + + int length, prev_len, col; + const AttrNumber offset = YBGetFirstLowInvalidAttributeNumber(rel); + col = -1; + prev_len = 0; + while ((col = bms_next_member(cols, col)) >= 0) + { + AttrNumber attnum = col + offset; + length = snprintf(NULL, 0, "%d (%d) ", attnum, col); + snprintf(&col_str[prev_len], length + 1, "%d (%d) ", attnum, col); + prev_len += length; + } + + elog(DEBUG2, "Relation: %u\t%s: %s", rel->rd_id, message, col_str); +} + +static void +YbLogInspectedColumns(Relation rel, const Bitmapset *updated_cols, + const Bitmapset *modified_cols, + const Bitmapset *unmodified_cols) +{ + if (log_min_messages >= DEBUG1) + return; + + YbLogColumnList(rel, modified_cols, + "Columns that are inspected and modified"); + + YbLogColumnList(rel, unmodified_cols, + "Columns that are inspected and unmodified"); + + YbLogColumnList(rel, updated_cols, "Columns that are marked for update"); +} + +static bool +YbIsColumnComparisonAllowed(const Bitmapset *modified_cols, + const Bitmapset *unmodified_cols) +{ + return ( + (bms_num_members(modified_cols) + bms_num_members(unmodified_cols)) <= + yb_update_optimization_options.num_cols_to_compare); +} + +/* ---------------------------------------------------------------- + * YBEqualDatums + * + * Function compares values of lhs and rhs datums with respect to value type + * and collation. + * + * Returns true in case value of lhs and rhs datums match. + * ---------------------------------------------------------------- + */ +static bool +YBEqualDatums(Datum lhs, Datum rhs, Oid atttypid, Oid collation) +{ + LOCAL_FCINFO(locfcinfo, 2); + TypeCacheEntry *typentry = + lookup_type_cache(atttypid, TYPECACHE_CMP_PROC_FINFO); + if (!OidIsValid(typentry->cmp_proc_finfo.fn_oid)) + ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg("could not identify a comparison function for " + "type %s", + format_type_be(typentry->type_id)))); + + /* To ensure that there is an upper bound on the size of data compared */ + const int lhslength = datumGetSize(lhs, typentry->typbyval, typentry->typlen); + const int rhslength = datumGetSize(rhs, typentry->typbyval, typentry->typlen); + + if (lhslength != rhslength || + lhslength > yb_update_optimization_options.max_cols_size_to_compare) + return false; + + InitFunctionCallInfoData(*locfcinfo, &typentry->cmp_proc_finfo, 2, collation, + NULL, NULL); + locfcinfo->args[0].value = lhs; + locfcinfo->args[0].isnull = false; + locfcinfo->args[1].value = rhs; + locfcinfo->args[1].isnull = false; + return DatumGetInt32(FunctionCallInvoke(locfcinfo)) == 0; +} + +/* ---------------------------------------------------------------------------- + * YBIsColumnModified + * + * Function to figure out if a given column identified by 'attnum' + * belonging to a relation 'rel' is modified as part of an update operation. + * This function assumes that both the old and new tuples are populated. + * Note - This function returns false if both the old and the new values are + * NULL. This is because changing a value from NULL to NULL does not modify the + * underlying datum. + * ---------------------------------------------------------------------------- + */ +static bool +YBIsColumnModified(Relation rel, HeapTuple oldtuple, HeapTuple newtuple, + const FormData_pg_attribute *attdesc) +{ + const AttrNumber attnum = attdesc->attnum; + + /* Skip virtual (system) and dropped columns */ + if (!IsRealYBColumn(rel, attnum)) + return false; + + bool old_is_null = false; + bool new_is_null = false; + TupleDesc relTupdesc = RelationGetDescr(rel); + Datum old_value = heap_getattr(oldtuple, attnum, relTupdesc, &old_is_null); + Datum new_value = heap_getattr(newtuple, attnum, relTupdesc, &new_is_null); + + return ( + (old_is_null != new_is_null) || + (!old_is_null && !YBEqualDatums(old_value, new_value, attdesc->atttypid, + attdesc->attcollation))); +} + +/* ---------------------------------------------------------------- + * YBComputeExtraUpdatedCols + * + * Function to compute a list of columns (that are not in that query's + * targetlist) that are modified by before-row triggers. It returns a set of + * columns that are modified and a set of columns that are unmodified. + * If this function is invoked with update optimizations turned off, it compares + * all the columns that are not in updated_cols. + * When update optimizations are turned on, it compares only those columns that + * have column-specific after-row triggers defined on them. Any other column + * that has been not been compared is marked as modified. + * ---------------------------------------------------------------- + */ +static void +YBComputeExtraUpdatedCols(Relation rel, HeapTuple oldtuple, HeapTuple newtuple, + Bitmapset *updated_cols, Bitmapset **modified_cols, + Bitmapset **unmodified_cols, + bool is_onconflict_update) +{ + Bitmapset *trig_cond_cols = NULL; + Bitmapset *skip_cols = updated_cols; + /* + * If the optimization is not enabled, we only need to compare the + * columns that are not in the targetlist of the query. + */ + if (YbIsUpdateOptimizationEnabled()) + { + /* + * If the optimization is enabled, we have already compared all the + * columns that might save us a round trip. In most cases, we can simply + * assume that all of the uncompared columns are indeed modified. + * However, if the relation has column-specific after-row triggers, we + * need to determine if any of the columns on which the conditional + * triggers are dependent, have been changed by the before-row triggers + * that have already fired. The deferrability of the after-row triggers + * is irrelevant in this context. + * + * Note - trigdesc cannot be NULL because it has been established that + * the relation has before-row triggers. + * TODO(kramanathan) - Reevaluate this logic when we support generated + * columns in PG15 (#23350). + */ + for (int idx = 0; idx < rel->trigdesc->numtriggers; ++idx) + { + Trigger *trigger = &rel->trigdesc->triggers[idx]; + if (!(trigger->tgnattr && (trigger->tgtype & TRIGGER_TYPE_AFTER) && + ((trigger->tgtype & TRIGGER_TYPE_UPDATE) || + (is_onconflict_update && (trigger->tgtype & TRIGGER_TYPE_INSERT))))) + continue; + + /* + * We are only interested in UPDATE and INSERT triggers in case of + * ON-CONFLICT DO UPDATE clauses. + */ + for (int col_idx = 0; col_idx < trigger->tgnattr; col_idx++) + { + const int bms_idx = trigger->tgattr[col_idx] - + YBGetFirstLowInvalidAttributeNumber(rel); + trig_cond_cols = bms_add_member(trig_cond_cols, bms_idx); + } + } + + /* Subtract the cols that have already been compared. */ + Bitmapset *compared_cols = bms_union(*unmodified_cols, *modified_cols); + trig_cond_cols = bms_difference(trig_cond_cols, compared_cols); + skip_cols = bms_union(skip_cols, compared_cols); + } + TupleDesc tupleDesc = RelationGetDescr(rel); + const AttrNumber offset = YBGetFirstLowInvalidAttributeNumber(rel); + + for (int idx = 0; idx < tupleDesc->natts; ++idx) + { + const FormData_pg_attribute *attdesc = TupleDescAttr(tupleDesc, idx); + const AttrNumber bms_idx = attdesc->attnum - offset; + if (bms_is_member(bms_idx, skip_cols) && + !bms_is_member(bms_idx, trig_cond_cols)) + continue; + + if (YBIsColumnModified(rel, oldtuple, newtuple, attdesc)) + *modified_cols = bms_add_member(*modified_cols, bms_idx); + else + *unmodified_cols = bms_add_member(*unmodified_cols, bms_idx); + } +} + +static void +YbUpdateHandleModifiedField(YbBitMatrix *matrix, Bitmapset **modified_entities, + int entity_idx, int field_idx, + Bitmapset **modified_cols, AttrNumber bms_idx) +{ + /* + * There is no need to check entities < row_idx as the algorithm guarantees + * that this entity is the first one that references this column. + */ + int local_entity_idx = entity_idx - 1; + while ((local_entity_idx = YbGetNextEntityFromBitMatrix( + matrix, field_idx, local_entity_idx)) >= 0) + *modified_entities = bms_add_member(*modified_entities, entity_idx); + + *modified_cols = bms_add_member(*modified_cols, bms_idx); +} + +static void +YbUpdateHandleUnmodifiedField(YbBitMatrix *matrix, int field_idx, + Bitmapset **unmodified_cols, AttrNumber bms_idx) +{ + YbBitMatrixSetRow(matrix, field_idx, false); + *unmodified_cols = bms_add_member(*unmodified_cols, bms_idx); +} + +static void +YbUpdateHandleUnmodifiedEntity(YbUpdateAffectedEntities *affected_entities, + int entity_idx, + YbSkippableEntities *skip_entities) +{ + struct YbUpdateEntity *entity = &affected_entities->entity_list[entity_idx]; + YbAddEntityToSkipList(entity->etype, entity->oid, skip_entities); +} + +/* ---------------------------------------------------------------- + * YbComputeModifiedEntities + * + * This function computes the entities (indexes, constraints) that need updates + * as part of the ModifyTable query, returning a collection of skippable + * entities. + * + * To compute updated entities, the old (current) and new (updated) values of + * columns (referenced by the entity) are compared. This comparison can be an + * expensive operation, especially when the column is large or when the column + * is of a composite/user defined type. To minimize the number of comparisons, + * this function prioritizes those columns whose modification could cause + * extra round trips to the storage layer in the form of index updates and + * constraint checks. + * This optimization is upped bounded in terms of the number of columns + * (GUC: yb_update_num_cols_to_compare) that are compared and the maximum size + * (GUC: yb_update_max_cols_size_to_compare) of a column that is compared. + * ---------------------------------------------------------------- + */ +static YbSkippableEntities * +YbComputeModifiedEntities(ResultRelInfo *resultRelInfo, HeapTuple oldtuple, + HeapTuple newtuple, Bitmapset **modified_cols, + Bitmapset **unmodified_cols, + YbUpdateAffectedEntities *affected_entities, + YbSkippableEntities *skip_entities) +{ + /* + * If the relation is simple ie. has no primary key, indexes, constraints or + * triggers, computation of affected entities will be skipped. + */ + if (affected_entities == NULL) + return NULL; + + /* + * Clone the update matrix to create a working copy for this tuple. We would + * like to preserve a clean copy for subsequent tuples in this query/plan. + */ + YbBitMatrix matrix; + YbCopyBitMatrix(&matrix, &affected_entities->matrix); + + Relation rel = resultRelInfo->ri_RelationDesc; + TupleDesc relTupdesc = RelationGetDescr(rel); + const int nentities = YB_UPDATE_AFFECTED_ENTITIES_NUM_ENTITIES(affected_entities); + + Bitmapset *modified_entities = + bms_del_member(bms_add_member(NULL, nentities), nentities); + + for (int entity_idx = 0; entity_idx < nentities; entity_idx++) + { + /* + * We need to make a decision for each entity that may be affected, + * adding the entity to a skip-list if all referenced columns are + * unmodified or proceeding with the relevant action if any of the + * columns are indeed modified. + */ + struct YbUpdateEntity *entity = &affected_entities->entity_list[entity_idx]; + + /* + * If we already know that the entity is modified, skip checking its + * columns. + */ + bool is_modified = bms_is_member(entity_idx, modified_entities); + + int field_idx = -1; + while (!is_modified && (field_idx = YbGetNextFieldFromBitMatrix( + &matrix, entity_idx, field_idx)) >= 0) + { + AttrNumber attnum = affected_entities->col_info_list[field_idx].attnum; + const int idx = YBBmsIndexToAttnum(rel, attnum); + const FormData_pg_attribute *attdesc = + TupleDescAttr(relTupdesc, idx); + if (!YbIsColumnComparisonAllowed(*modified_cols, *unmodified_cols) || + YBIsColumnModified(rel, oldtuple, newtuple, attdesc)) + { + /* + * If we have already exceeded the max number of columns + * that we are allowed to compare, assume that the column + * is changing in value. + */ + YbUpdateHandleModifiedField(&matrix, &modified_entities, + entity_idx, field_idx, + modified_cols, attnum); + break; + } + else + YbUpdateHandleUnmodifiedField(&matrix, field_idx, + unmodified_cols, attnum); + } + + if (bms_is_member(entity_idx, modified_entities)) + { + /* Mark the primary key as updated */ + if (entity->oid == rel->rd_pkindex) + { + /* + * In case the primary key is updated, the entire row must be + * deleted and re-inserted. The no update index list is not + * useful in such cases. + */ + skip_entities->index_list = NIL; + break; + } + } + else + YbUpdateHandleUnmodifiedEntity( + affected_entities, entity_idx, skip_entities); + } + + YbLogOptimizationSummary(affected_entities->entity_list, modified_entities, + nentities); + return skip_entities; +} + +/* ---------------------------------------------------------------- + * YbComputeModifiedColumnsAndSkippableEntities + * + * This function computes a list of columns that are modified by the query and + * a list of entities (indexes, constraints) whose bookkeeping updates can be + * skipped as a result of unmodified columns. + * A list of modified columns (updated_cols) is required: + * - To construct the payload of updated values to be send to the storage layer. + * - To determine if column-specific after-row triggers are elgible to fire. + * ---------------------------------------------------------------- + */ +void +YbComputeModifiedColumnsAndSkippableEntities( + ModifyTable *plan, ResultRelInfo *resultRelInfo, EState *estate, + HeapTuple oldtuple, HeapTuple newtuple, Bitmapset **updated_cols, + bool beforeRowUpdateTriggerFired) +{ + /* + * InvalidAttrNumber indicates that the whole row is to be changed. This + * happens when the primary key is modified. Performing any optimization is + * redundant in such a case. + */ + if (bms_is_member(InvalidAttrNumber, *updated_cols)) + return; + + /* + * There are scenarios where it is possible to update a tuple without + * reading in its current state (oldtuple) -- the update query fully + * specifies the primary key, the columns being updated do not have indexes + * or constraints on them, the values to which the columns are being + * updated are absolute and do not depend on the current values, and there + * are no "before-row" triggers. Modified columns cannot be computed in this + * scenario as there is nothing (oltuple) to compare against, and there is + * not much to be saved in doing this anyway. + */ + if (oldtuple == NULL) + return; + + Relation rel = resultRelInfo->ri_RelationDesc; + + /* + * Maintain two bitmapsets - one to track columns that have indeed been + * modified (after comparison of their old and new values) and another to + * track columns that remain unmodified. + * The intersection of these two bitmapsets is always a null set. + * Further, there may be columns that we chose to not compare because: + * - it does not affect any entity + * - we already know that all the entities it affects are indeed modified + * In other words, the union of unmodified_cols and modified_cols may NOT be + * equal to updated_cols. + * Additionally, the union of unmodified_cols and modified_cols may include + * columns that are not in updated_cols. + */ + Bitmapset *unmodified_cols = NULL; + Bitmapset *modified_cols = NULL; + + if (YbIsUpdateOptimizationEnabled()) + { + YbComputeModifiedEntities(resultRelInfo, oldtuple, newtuple, + &modified_cols, &unmodified_cols, + plan->yb_update_affected_entities, + &estate->yb_skip_entities); + } + + if (beforeRowUpdateTriggerFired) + { + YBComputeExtraUpdatedCols(rel, oldtuple, newtuple, *updated_cols, + &modified_cols, &unmodified_cols, + (plan->operation == CMD_INSERT && + plan->onConflictAction == ONCONFLICT_UPDATE)); + } + + *updated_cols = bms_del_members(*updated_cols, unmodified_cols); + *updated_cols = bms_add_members(*updated_cols, modified_cols); + + YbLogInspectedColumns(rel, *updated_cols, modified_cols, unmodified_cols); + + bms_free(unmodified_cols); + bms_free(modified_cols); +} + +bool +YbIsPrimaryKeyUpdated(Relation rel, const Bitmapset *updated_cols) +{ + return bms_overlap(YBGetTablePrimaryKeyBms(rel), updated_cols); +} diff --git a/src/postgres/src/backend/nodes/Makefile b/src/postgres/src/backend/nodes/Makefile index 1d175f388bf6..32e6ec42b12f 100644 --- a/src/postgres/src/backend/nodes/Makefile +++ b/src/postgres/src/backend/nodes/Makefile @@ -13,6 +13,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global OBJS = \ + ybbitmatrix.o \ ybtidbitmap.o \ \ bitmapset.o \ diff --git a/src/postgres/src/backend/nodes/copyfuncs.c b/src/postgres/src/backend/nodes/copyfuncs.c index 78fbe0eb8caa..c194c7b005a3 100644 --- a/src/postgres/src/backend/nodes/copyfuncs.c +++ b/src/postgres/src/backend/nodes/copyfuncs.c @@ -29,6 +29,8 @@ #include "utils/datum.h" #include "utils/rel.h" +/* YB includes */ +#include "nodes/ybbitmatrix.h" /* * Macros to simplify copying of different kinds of fields. Use these @@ -235,7 +237,8 @@ _copyModifyTable(const ModifyTable *from) COPY_NODE_FIELD(ybPushdownTlist); COPY_NODE_FIELD(ybReturningColumns); COPY_NODE_FIELD(ybColumnRefs); - COPY_NODE_FIELD(no_update_index_list); + COPY_NODE_FIELD(yb_skip_entities); + COPY_NODE_FIELD(yb_update_affected_entities); COPY_SCALAR_FIELD(no_row_trigger); COPY_SCALAR_FIELD(ybUseScanTupleInUpdate); COPY_SCALAR_FIELD(ybHasWholeRowAttribute); @@ -5326,6 +5329,30 @@ _copyYbBatchedExpr(const YbBatchedExpr *from) return newnode; } +static YbUpdateAffectedEntities * +_copyYbUpdateAffectedEntities(const YbUpdateAffectedEntities *from) +{ + YbUpdateAffectedEntities *newnode = makeNode(YbUpdateAffectedEntities); + + COPY_POINTER_FIELD(entity_list, from->matrix.ncols * sizeof(struct YbUpdateEntity)); + COPY_POINTER_FIELD(col_info_list, from->matrix.nrows * sizeof(struct YbUpdateColInfo)); + YbCopyBitMatrix(&newnode->matrix, &from->matrix); + + return newnode; +} + +static YbSkippableEntities * +_copyYbSkippableEntities(const YbSkippableEntities *from) +{ + YbSkippableEntities *newnode = makeNode(YbSkippableEntities); + + COPY_NODE_FIELD(index_list); + COPY_NODE_FIELD(referencing_fkey_list); + COPY_NODE_FIELD(referenced_fkey_list); + + return newnode; +} + /* * copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h * @@ -6318,6 +6345,14 @@ copyObjectImpl(const void *from) retval = _copyYbBatchedExpr(from); break; + case T_YbSkippableEntities: + retval = _copyYbSkippableEntities(from); + break; + + case T_YbUpdateAffectedEntities: + retval = _copyYbUpdateAffectedEntities(from); + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(from)); retval = 0; /* keep compiler quiet */ diff --git a/src/postgres/src/backend/nodes/outfuncs.c b/src/postgres/src/backend/nodes/outfuncs.c index 50584e91a5ff..803f8862344a 100644 --- a/src/postgres/src/backend/nodes/outfuncs.c +++ b/src/postgres/src/backend/nodes/outfuncs.c @@ -442,7 +442,8 @@ _outModifyTable(StringInfo str, const ModifyTable *node) WRITE_NODE_FIELD(ybPushdownTlist); WRITE_NODE_FIELD(ybReturningColumns); WRITE_NODE_FIELD(ybColumnRefs); - WRITE_NODE_FIELD(no_update_index_list); + WRITE_NODE_FIELD(yb_skip_entities); + WRITE_NODE_FIELD(yb_update_affected_entities); WRITE_BOOL_FIELD(no_row_trigger); WRITE_BOOL_FIELD(ybUseScanTupleInUpdate); WRITE_BOOL_FIELD(ybHasWholeRowAttribute); @@ -4037,6 +4038,23 @@ _outYbExprColrefDesc(StringInfo str, const YbExprColrefDesc *node) WRITE_OID_FIELD(collid); } +static void +_outYbSkippableEntities(StringInfo str, const YbSkippableEntities *node) +{ + WRITE_NODE_TYPE("YBSKIPPABLEENTITIES"); + + WRITE_NODE_FIELD(index_list); + WRITE_NODE_FIELD(referencing_fkey_list); + WRITE_NODE_FIELD(referenced_fkey_list); +} + +static void +_outYbUpdateAffectedEntities(StringInfo str, const YbUpdateAffectedEntities *node) +{ + WRITE_NODE_TYPE("YBUPDATEAFFECTEDENTITIES"); + /* TODO(kramanathan): Define serializability for YbUpdateAffectedEntities */ +} + /* * outNode - * converts a Node into ascii string and append it to 'str' @@ -4766,6 +4784,12 @@ outNode(StringInfo str, const void *obj) case T_YbExprColrefDesc: _outYbExprColrefDesc(str, obj); break; + case T_YbSkippableEntities: + _outYbSkippableEntities(str, obj); + break; + case T_YbUpdateAffectedEntities: + _outYbUpdateAffectedEntities(str, obj); + break; default: diff --git a/src/postgres/src/backend/nodes/readfuncs.c b/src/postgres/src/backend/nodes/readfuncs.c index fc32afad847a..2a4e1a516352 100644 --- a/src/postgres/src/backend/nodes/readfuncs.c +++ b/src/postgres/src/backend/nodes/readfuncs.c @@ -1745,7 +1745,8 @@ _readModifyTable(void) READ_NODE_FIELD(ybPushdownTlist); READ_NODE_FIELD(ybReturningColumns); READ_NODE_FIELD(ybColumnRefs); - READ_NODE_FIELD(no_update_index_list); + READ_NODE_FIELD(yb_skip_entities); + READ_NODE_FIELD(yb_update_affected_entities); READ_BOOL_FIELD(no_row_trigger); READ_BOOL_FIELD(ybUseScanTupleInUpdate); READ_BOOL_FIELD(ybHasWholeRowAttribute); @@ -2906,6 +2907,25 @@ _readYbExprColrefDesc(void) READ_DONE(); } +static YbSkippableEntities * +_readYbSkippableEntities(void) +{ + READ_LOCALS(YbSkippableEntities); + + READ_NODE_FIELD(index_list); + READ_NODE_FIELD(referencing_fkey_list); + READ_NODE_FIELD(referenced_fkey_list); + + READ_DONE(); +} + +static YbUpdateAffectedEntities * +_readYbUpdateAffectedEntities(void) +{ + /* TODO(kramanathan): Define serializability for YbUpdateAffectedEntities */ + return NULL; +} + /* * parseNodeString * @@ -3191,6 +3211,10 @@ parseNodeString(void) return_value = _readPartitionRangeDatum(); else if (MATCH("YBEXPRCOLREFDESC", 16)) return_value = _readYbExprColrefDesc(); + else if (MATCH("YBSKIPPABLEENTITIES", 19)) + return_value = _readYbSkippableEntities(); + else if (MATCH("YBUPDATEAFFECTEDENTITIES", 24)) + return_value = _readYbUpdateAffectedEntities(); else { elog(ERROR, "badly formatted node string \"%.32s\"...", token); diff --git a/src/postgres/src/backend/nodes/ybbitmatrix.c b/src/postgres/src/backend/nodes/ybbitmatrix.c new file mode 100644 index 000000000000..723b6037150e --- /dev/null +++ b/src/postgres/src/backend/nodes/ybbitmatrix.c @@ -0,0 +1,195 @@ +/*------------------------------------------------------------------------- + * + * ybbitmatrix.c + * Yugabyte bit matrix package + * + * This module provides a boolean matrix data structure that is internally + * implemented as a bitmapset (nodes/bitmapset.h). This allows for efficient + * storage and row-level operations. + * + * + * 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. + * + * IDENTIFICATION + * src/backend/nodes/ybbitmatrix.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include + +#include "nodes/ybbitmatrix.h" + +static inline void +check_matrix_invariants(const YbBitMatrix *matrix) +{ + if (!matrix || !matrix->data) + elog(ERROR, "YB bitmatrix not defined"); +} + +static inline void +check_column_invariants(const YbBitMatrix *matrix, int col_idx) +{ + if (col_idx >= matrix->ncols) + elog(ERROR, + "column index must be between [0, %" PRId32 "] in YB bitmatrix", + matrix->ncols - 1); +} + +static inline void +check_row_invariants(const YbBitMatrix *matrix, int row_idx) +{ + if (row_idx >= matrix->nrows) + elog(ERROR, + "row index must be between [0, %" PRId32 "] in YB bitmatrix", + matrix->nrows - 1); +} + +void +YbInitBitMatrix(YbBitMatrix *matrix, int nrows, int ncols) +{ + Assert(!matrix->data); + + matrix->nrows = nrows; + matrix->ncols = ncols; + const int max_idx = matrix->nrows * matrix->ncols; + if (max_idx) + matrix->data = bms_del_member(bms_add_member(NULL, max_idx), max_idx); +} + +void +YbCopyBitMatrix(YbBitMatrix *dest, const YbBitMatrix *src) +{ + *dest = *src; + dest->data = bms_copy(src->data); +} + +void +YbFreeBitMatrix(YbBitMatrix *matrix) +{ + bms_free(matrix->data); +} + +int +YbBitMatrixNumRows(const YbBitMatrix *matrix) +{ + return matrix->nrows; +} + +int +YbBitMatrixNumCols(const YbBitMatrix *matrix) +{ + return matrix->ncols; +} + +void +YbBitMatrixSetRow(YbBitMatrix *matrix, int row_idx, bool value) +{ + check_matrix_invariants(matrix); + check_row_invariants(matrix, row_idx); + + int lower = row_idx * matrix->ncols; + int upper = lower + matrix->ncols - 1; + + /* + * TODO(kramanathan): Optimize extra palloc in setting values to false. + * This can be done be implementing a bms_del_range function. + */ + matrix->data = value ? bms_add_range(matrix->data, lower, upper) : + bms_del_members(matrix->data, + bms_add_range(NULL, lower, upper)); +} + +/* + * This function returns the smallest row greater than "prev_row" that has a + * member in the given column, or -2 if there is none. + * "prev_row" must NOT be less than -1, or the behavior is unpredictable. + * + * This is intended as support for iterating through the members of a set. + * The typical pattern is + * + * x = -1; + * while ((x = YbBitMatrixNextMemberInColumn(inputmatrix, col, row_x)) >= 0) + * process member row_x; + * + * Notice that when there are no more members, we return -2, not -1 as you + * might expect. The rationale for that is to allow distinguishing the + * loop-not-started state (x == -1) from the loop-completed state (x == -2). + * It makes no difference in simple loop usage, but complex iteration logic + * might need such an ability. + * This semantics of this function are based on bms_next_member/bms_prev_member + * in bitmapset.c. + */ +int +YbBitMatrixNextMemberInColumn(const YbBitMatrix *matrix, int col_idx, + int prev_row) +{ + check_matrix_invariants(matrix); + check_column_invariants(matrix, col_idx); + + for (int row_idx = prev_row + 1, cell_idx = (row_idx * matrix->ncols) + col_idx; + row_idx < matrix->nrows; + row_idx++, cell_idx += matrix->ncols) + { + if (bms_is_member(cell_idx, matrix->data)) + return row_idx; + } + + return -2; +} + +/* + * This function returns the smallest column greater than "prev_col" that has a + * member in the given row, or -2 if there is none. + * "prev_col" must NOT be less than -1, or the behavior is unpredictable. + * + * For semantics, see note on YbBitMatrixNextMemberInColumn. + */ +int +YbBitMatrixNextMemberInRow(const YbBitMatrix *matrix, int row_idx, int prev_col) +{ + check_matrix_invariants(matrix); + check_row_invariants(matrix, row_idx); + + int prevbit = (row_idx * matrix->ncols) + prev_col; + int nextbit = bms_next_member(matrix->data, prevbit); + int result = nextbit - prevbit + prev_col; + if (result > prev_col && result < matrix->ncols) + return result; + + return -2; +} + +bool +YbBitMatrixGetValue(const YbBitMatrix *matrix, int row_idx, int col_idx) +{ + check_matrix_invariants(matrix); + check_column_invariants(matrix, col_idx); + check_row_invariants(matrix, row_idx); + + return bms_is_member((row_idx * matrix->ncols) + col_idx, matrix->data); +} + +void +YbBitMatrixSetValue(YbBitMatrix *matrix, int row_idx, int col_idx, bool value) +{ + check_matrix_invariants(matrix); + check_column_invariants(matrix, col_idx); + check_row_invariants(matrix, row_idx); + + const int idx = (row_idx * matrix->ncols) + col_idx; + matrix->data = value ? bms_add_member(matrix->data, idx) : + bms_del_member(matrix->data, idx); +} diff --git a/src/postgres/src/backend/optimizer/plan/createplan.c b/src/postgres/src/backend/optimizer/plan/createplan.c index 492ec00fcb5d..da749c75f387 100644 --- a/src/postgres/src/backend/optimizer/plan/createplan.c +++ b/src/postgres/src/backend/optimizer/plan/createplan.c @@ -50,9 +50,10 @@ #include "utils/syscache.h" #include "utils/rel.h" -#include "pg_yb_utils.h" +/* YB includes */ #include "access/yb_scan.h" #include "optimizer/ybcplan.h" +#include "pg_yb_utils.h" /* * Flag bits that can appear in the flags argument of create_plan_recurse(). @@ -3830,6 +3831,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) List *returning_cols = NIL; List *column_refs = NIL; + bool yb_is_single_row_update_or_delete; /* * If we are a single row UPDATE/DELETE in a YB relation, add Result subplan @@ -3837,11 +3839,11 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) * running outside of a transaction and thus cannot rely on the results from a * separately executed operation. */ - if (yb_single_row_update_or_delete_path(root, best_path, &modify_tlist, - &column_refs, &result_tlist, - &returning_cols, &no_row_trigger, - best_path->operation == CMD_UPDATE ? - &no_update_index_list : NULL)) + yb_is_single_row_update_or_delete = yb_single_row_update_or_delete_path( + root, best_path, &modify_tlist, &column_refs, &result_tlist, + &returning_cols, &no_row_trigger, + best_path->operation == CMD_UPDATE ? &no_update_index_list : NULL); + if (yb_is_single_row_update_or_delete) { subplan = (Plan *) make_result(result_tlist, NULL, NULL); copy_generic_path_info(subplan, best_path->subpath); @@ -3873,11 +3875,66 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) plan->ybPushdownTlist = modify_tlist; plan->ybReturningColumns = returning_cols; plan->ybColumnRefs = column_refs; - plan->no_update_index_list = no_update_index_list; plan->no_row_trigger = no_row_trigger; + plan->yb_skip_entities = YbInitSkippableEntities(no_update_index_list); copy_generic_path_info(&plan->plan, &best_path->path); + /* + * TODO(kramanathan): Evaluate whether the equivalent of "is single row + * update" is need for ON CONFLICT DO UPDATE. + */ + if (YbIsUpdateOptimizationEnabled() && + ((!yb_is_single_row_update_or_delete && plan->operation == CMD_UPDATE) || + (plan->operation == CMD_INSERT && plan->onConflictAction == ONCONFLICT_UPDATE))) + { + RangeTblEntry *rte = NULL; + if (!root->simple_rel_array_size) + { + /* + * This is a simple INSERT ON CONFLICT with an empty join tree. + * This query involves exactly one relation. It may however have + * several range table entries, but the first entry always contains + * information about the ON CONFLICT DO UPDATE. + */ + rte = lfirst(list_head(root->parse->rtable)); + } + else + { + /* + * During execution of ModifyTable, the first entry in + * resultRelations is hardcoded to be always used. Use the + * same relation to compute the list of affected entities. + * This also handles situations when the modifyTable is being run on + * a view rather than a relation. + */ + int rt_index = lfirst_int(list_head(plan->resultRelations)); + rte = root->simple_rte_array[rt_index]; + } + + Relation rel = RelationIdGetRelation(rte->relid); + /* + * The memory allocations here are made in the context of the plan's + * memory context and will be freed up when the plan is destroyed: + * - if it is decided that the plan will not be cached and subsequently + * dropped (see prepare.c for more details). + * - if the plan is chosen to be cached, the plan's memory context + * is re-parented under the CacheMemoryContext, in which case it is + * destroyed via DEALLOCATE of the prepared statement. + * An exception to this is one-shot plans, which is currently used only + * by SPI. In this case, it is the responsibility of the caller to drop + * the memory context associated with the plan. + * TODO(kramanathan): Reevaluate this in the context of plan_cache_mode + * in PG15. (#23350) + * TODO(kramanathan): Add support for partitioned tables. (#23348) + */ + if (rel->rd_rel->relkind == RELKIND_RELATION) + plan->yb_update_affected_entities = + YbComputeAffectedEntitiesForRelation(plan, rel, rte->updatedCols); + + RelationClose(rel); + } + return plan; } @@ -9044,7 +9101,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan, /* These are set separately only if needed. */ node->ybPushdownTlist = NIL; node->ybReturningColumns = NIL; - node->no_update_index_list = NIL; + node->yb_skip_entities = NULL; + node->yb_update_affected_entities = NULL; node->no_row_trigger = false; /* diff --git a/src/postgres/src/backend/optimizer/util/ybcplan.c b/src/postgres/src/backend/optimizer/util/ybcplan.c index 72cc8ab62efb..ae39552f9e39 100644 --- a/src/postgres/src/backend/optimizer/util/ybcplan.c +++ b/src/postgres/src/backend/optimizer/util/ybcplan.c @@ -39,11 +39,37 @@ #include "utils/lsyscache.h" /* YB includes. */ +#include "catalog/catalog.h" #include "catalog/pg_am_d.h" #include "catalog/yb_catalog_version.h" #include "yb/yql/pggate/ybc_pggate.h" #include "pg_yb_utils.h" +/* + * A mapping between columns and their position/index on the col_info_list. + * The keys in the map are a zero-indexed version of the column's attr number. + * The values are the position/index of the columns in the col_info_list of the + * update metadata. + * This structure allows us to sort the columns arbitrarily, while retaining the + * ability to access them in constant time, given an attribute number. + * + * The number of elements in the map is equal to the number of columns that may + * be potentially modified by the update query. + */ +typedef size_t *AttrToColIdxMap; + +static void YbUpdateComputeIndexColumnReferences( + const Relation rel, const Bitmapset *maybe_modified_cols, + const AttrToColIdxMap map, YbUpdateAffectedEntities *affected_entities, + YbSkippableEntities *skip_entities, int *nentities); + +static void YbUpdateComputeForeignKeyColumnReferences( + const Relation rel, const List *fkeylist, + const Bitmapset *maybe_modified_cols, const AttrToColIdxMap map, + YbUpdateAffectedEntities *affected_entities, + YbSkippableEntities *skip_entities, bool is_referencing_rel, + int *nentities); + /* * Check if statement can be implemented by a single request to the DocDB. * @@ -323,3 +349,594 @@ extract_pushdown_clauses(List *restrictinfo_list, } } } + +static YbUpdateAffectedEntities * +YbInitUpdateAffectedEntities(Relation rel, int nfields, int maxentities) +{ + YbUpdateAffectedEntities *affected_entities = makeNode(YbUpdateAffectedEntities); + + affected_entities->col_info_list = + palloc0(nfields * (sizeof(struct YbUpdateColInfo))); + + /* + * There is a possibility that not all the entities in maxentities will be + * affected by the update. However, we allocate the extra space here to + * avoid doing multiple allocations. We can also realloc to shrink the + * space later, but that probably isn't worth the hassle. + */ + affected_entities->entity_list = + palloc0(maxentities * sizeof(struct YbUpdateEntity)); + + return affected_entities; +} + +YbSkippableEntities * +YbInitSkippableEntities(List *no_update_index_list) +{ + YbSkippableEntities *skip_entities = makeNode(YbSkippableEntities); + skip_entities->index_list = no_update_index_list; + return skip_entities; +} + +void +YbCopySkippableEntities(YbSkippableEntities *dst, + const YbSkippableEntities *src) +{ + if (src == NULL) + return; + + dst->index_list = list_copy(src->index_list); + dst->referencing_fkey_list = list_copy(src->referencing_fkey_list); + dst->referenced_fkey_list = list_copy(src->referenced_fkey_list); +} + +/* ---------------------------------------------------------------- + * YbAddEntityToSkipList + * + * Function to add the given entity to the appropriate skip list based on its + * type. + * ---------------------------------------------------------------- + */ +void +YbAddEntityToSkipList(YbSkippableEntityType etype, Oid oid, + YbSkippableEntities *skip_entities) +{ + List **skip_list; + switch (etype) + { + case SKIP_PRIMARY_KEY: + /* Nothing to be done here */ + return; + case SKIP_SECONDARY_INDEX: + skip_list = &(skip_entities->index_list); + break; + case SKIP_REFERENCING_FKEY: + skip_list = &(skip_entities->referencing_fkey_list); + break; + case SKIP_REFERENCED_FKEY: + skip_list = &(skip_entities->referenced_fkey_list); + break; + default: + elog(ERROR, "Unsupported update entity type: %d", etype); + return; + } + + *skip_list = lappend_oid(*skip_list, oid); +} + +/* ---------------------------------------------------------------- + * YbClearSkippableEntities + * + * Function to clear the contents of YbSkippableEntities. + * Note - This does not free the memory associated with YbSkippableEntities + * struct itself. + * ---------------------------------------------------------------- + */ +void +YbClearSkippableEntities(YbSkippableEntities *skip_entities) +{ + if (!skip_entities) + return; + + list_free(skip_entities->index_list); + list_free(skip_entities->referencing_fkey_list); + list_free(skip_entities->referenced_fkey_list); +} + +static AttrToColIdxMap +YbInitAttrToIdxMap(const Relation rel, const Bitmapset *maybe_modified_cols, + size_t maxcols) +{ + /* + * Construct a map between the attnum of a column to its position in a list + * of potentially modified columns. Account for the fact that attnums are + * 1-indexed and are offset by 'YBFirstLowInvalidAttributeNumber' or + * 'FirstLowInvalidHeapAttributeNumber' in the bitmapset. + */ + AttrToColIdxMap attr_to_idx_map = palloc0(maxcols * (sizeof(size_t))); + size_t idx = 0; + int bms_idx = -1; + + while ((bms_idx = bms_next_member(maybe_modified_cols, bms_idx)) >= 0) + { + attr_to_idx_map[YBBmsIndexToAttnum(rel, bms_idx)] = idx; + ++idx; + } + + return attr_to_idx_map; +} + +static void +YbInitUpdateEntity(YbUpdateAffectedEntities *affected_entities, size_t idx, + Oid oid, YbSkippableEntityType etype) +{ + affected_entities->entity_list[idx].oid = oid; + affected_entities->entity_list[idx].etype = etype; +} + +/* ---------------------------------------------------------------- + * YbAddColumnReference + * + * Function to record the fact the given entity references the given column. + * The bitmapset index of the column is converted into a zero-indexed number + * (colattr - 1) so that the relationship can be recorded in a zero-indexed + * array of columns. + * + * ---------------------------------------------------------------- + */ +static void +YbAddColumnReference(const Relation rel, + YbUpdateAffectedEntities *affected_entities, + AttrToColIdxMap map, int bms_idx, size_t ref_entity_idx) +{ + size_t idx = map[YBBmsIndexToAttnum(rel, bms_idx)]; + struct YbUpdateColInfo *col_info = &affected_entities->col_info_list[idx]; + col_info->attnum = bms_idx; + col_info->entity_refs = + lappend_int(col_info->entity_refs, (int) ref_entity_idx); +} + +/* ---------------------------------------------------------------- + * YbConsiderColumnAndEntityForUpdate + * + * Function to record the relationship between unique pairs of (entity, column) + * in the context of an UPDATE query. + * + * ---------------------------------------------------------------- + */ +static void +YbConsiderColumnAndEntityForUpdate(const Relation rel, + YbUpdateAffectedEntities *affected_entities, + AttrToColIdxMap map, AttrNumber colattr, + Oid oid, YbSkippableEntityType etype, + int *nentities) +{ + /* + * Check if the entity has already been inserted by a previous column + * that references the entity. If not, add the entity to the list. + */ + if (!(*nentities) || affected_entities->entity_list[*nentities - 1].oid != oid) + { + YbInitUpdateEntity(affected_entities, *nentities, oid, etype); + ++(*nentities); + } + + YbAddColumnReference(rel, affected_entities, map, colattr, *nentities - 1); +} + +static void +YbLogUpdateMatrix(const YbUpdateAffectedEntities *affected_entities) +{ + if (log_min_messages >= DEBUG1) + return; + + const int nfields = YB_UPDATE_AFFECTED_ENTITIES_NUM_FIELDS(affected_entities); + if (!nfields) + { + elog(DEBUG2, "No columns benefit from update optimization"); + return; + } + + const int nentities = YB_UPDATE_AFFECTED_ENTITIES_NUM_ENTITIES(affected_entities); + if (!nentities) + { + elog(DEBUG2, "No entities benefit from update optimization"); + return; + } + + int length, prev_len, val_len, header_len; + val_len = Max(nfields * 2, 10); + header_len = Max(nfields * 3, 10); + + char *vals = (char *) palloc0(val_len * sizeof(char)); + char *headers = (char *) palloc0(header_len * sizeof(char)); + + /* Print headers */ + headers[0] = '-'; + headers[1] = '\t'; + prev_len = 2; /* for '-\t' chars */ + for (int j = 0; j < nentities; j++) + { + AttrNumber attnum = affected_entities->col_info_list[j].attnum; + /* Check if the column is populated */ + if (!attnum) + break; + length = snprintf(NULL, 0, "\t%d", attnum); + snprintf(&headers[prev_len], length + 1, "\t%d", attnum); + prev_len += length; + } + + elog(DEBUG2, "Update matrix: rows represent OID of entities, columns represent attnum of cols"); + elog(DEBUG2, "%s", headers); + + /* Print body */ + for (int i = 0; i < nentities; i++) + { + for (int j = 0; j < nfields; j++) + { + /* Check if the column is populated */ + if (!affected_entities->col_info_list[j].attnum) + break; + vals[j * 2] = + YbBitMatrixGetValue(&affected_entities->matrix, j, i) ? 'Y' : '-'; + vals[j * 2 + 1] = '\t'; + } + + elog(DEBUG2, "%d\t%s", affected_entities->entity_list[i].oid, vals); + } + + pfree(headers); + pfree(vals); +} + +/* ---------------------------------------------------------------- + * YbGetMaybeModifiedCols - Pessimistically computes a bitmapset of columns + * that the given query has the potential to modify. + * + * This function is invoked at planning time, so the only information used to + * construct this list is a bitmapset of columns that we have permission to + * update for the given query, and whether the relation has any before row + * triggers. + * ---------------------------------------------------------------- + */ +static Bitmapset * +YbGetMaybeModifiedCols(Relation rel, Bitmapset *update_attrs) +{ + TupleDesc desc = RelationGetDescr(rel); + + /* + * A BEFORE ROW UPDATE trigger could have modified any of the columns in the + * tuple. In such a case, we do not eliminate any column from consideration + * in this stage. + * TODO(kramanathan) - Account for disabled triggers. This is not very hard + * to do, but it involves refactoring code in trigger.c so that it can be + * imported at planning time. Vanilla postgres does most of its trigger + * checks at execution time. + */ + if (rel->trigdesc && rel->trigdesc->trig_update_before_row) + { + Bitmapset *maybe_modified_cols = NULL; + const AttrNumber offset = YBGetFirstLowInvalidAttributeNumber(rel); + for (int idx = 0; idx < desc->natts; ++idx) + { + const FormData_pg_attribute *attdesc = TupleDescAttr(desc, idx); + const AttrNumber bms_idx = attdesc->attnum - offset; + maybe_modified_cols = bms_add_member(maybe_modified_cols, bms_idx); + } + + return maybe_modified_cols; + } + + /* + * Fetch a list of columns on which we have permission to do updates + * as the targetlist in the plan is not accurate. + */ + return bms_copy(update_attrs); +} + +/* + * YbQsortCompareColRefsList -- Comparator function to sort columns in + * decreasing order of the number of entities referencing the column. + */ +static int +YbQsortCompareColRefsList(const void *a, const void *b) +{ + const struct YbUpdateColInfo *acol_info = (const struct YbUpdateColInfo *) a; + const struct YbUpdateColInfo *bcol_info = (const struct YbUpdateColInfo *) b; + + if (acol_info == NULL || acol_info->entity_refs == NULL) + return 1; + + if (bcol_info == NULL || bcol_info->entity_refs == NULL) + return -1; + + return acol_info->entity_refs->length > bcol_info->entity_refs->length ? -1 : 1; +} + +static void +YbPopulateUpdateMatrix(YbUpdateAffectedEntities *affected_entities, + int nentities, + int nfields) +{ + YbInitBitMatrix(&affected_entities->matrix, nfields, nentities); + + ListCell *refCell; + for (int field_idx = 0; field_idx < nfields; field_idx++) + { + foreach (refCell, affected_entities->col_info_list[field_idx].entity_refs) + { + YbBitMatrixSetValue(&affected_entities->matrix, field_idx, + lfirst_int(refCell), true /* value */); + } + } + + YbLogUpdateMatrix(affected_entities); +} + +/* ---------------------------------------------------------------- + * YbComputeAffectedEntitiesForRelation + * + * Computes a list of entities (indexes, foreign keys, etc.) that may be + * affected by the given ModifyTable (update) plan. This information is used + * during the execution of the statement to determine if bookkeeping operations + * on a subset of these entities can be skipped, thus reducing the amount of + * work done by the system. This function is invoked at planning time so that + * the results can be cached and reused in prepared statements. + * + * The list of entities examined are those that can trigger a request to the + * storage layer. Scenarios where this may have happen include the following: + * - Primary Key updates + * - Secondary Index updates + * - Foreign Key Constraints (both as a 'referencing' relation and as a + * 'referenced' relation) + * - Uniqueness Constraints (implemented as secondary indexes) + * + * Other types of constraints do not benefit from this optimization because + * they are either: + * - Not pushed down and thus do not need this optimization, such as + * CHECK constraints (OR) + * - Not supported, such as exclusion contraints (#3944) and constraint + * triggers (#1709). Re-evaluate when implemented. + * + * ---------------------------------------------------------------- + */ +YbUpdateAffectedEntities * +YbComputeAffectedEntitiesForRelation(ModifyTable *modifyTable, + const Relation rel, + Bitmapset *update_attrs) +{ + if (!YbIsUpdateOptimizationEnabled()) + return NULL; + + /* Fetch a list of candidate entities that may be impacted by the update. */ + List *fkeylist = copyObject(RelationGetFKeyList(rel)); + List *fkeyreflist = YbRelationGetFKeyReferencedByList(rel); + + const int maxentities = list_length(rel->rd_indexlist) + + (rel->rd_pkindex != InvalidOid ? 1 : 0) + + (fkeylist ? fkeylist->length : 0) + + (fkeyreflist ? fkeyreflist->length : 0); + + /* If there are no entities that benefit from the optimization, skip it */ + if (!maxentities) + return NULL; + + Bitmapset *maybe_modified_cols = YbGetMaybeModifiedCols(rel, update_attrs); + const int nfields = bms_num_members(maybe_modified_cols); + YbUpdateAffectedEntities *affected_entities = + YbInitUpdateAffectedEntities(rel, nfields, maxentities); + + TupleDesc desc = RelationGetDescr(rel); + AttrToColIdxMap map = + YbInitAttrToIdxMap(rel, maybe_modified_cols, desc->natts + 1); + + int nentities = 0; + /* + * Compute a list of indexes (primary or secondary) that potentially need to + * be updated. + */ + YbUpdateComputeIndexColumnReferences(rel, maybe_modified_cols, map, + affected_entities, + modifyTable->yb_skip_entities, + &nentities); + + /* Compute a list of affected 'referencing' foreign key constraints */ + YbUpdateComputeForeignKeyColumnReferences( + rel, fkeylist, maybe_modified_cols, map, affected_entities, + modifyTable->yb_skip_entities, true /* is_referencing_rel */, + &nentities); + + /* Compute a list of affected 'referenced' foreign key constraints */ + YbUpdateComputeForeignKeyColumnReferences( + rel, fkeyreflist, maybe_modified_cols, map, affected_entities, + modifyTable->yb_skip_entities, false /* is_referencing_rel */, + &nentities); + + Assert(nentities <= maxentities); + + /* + * Sort the columns in descending order of the number of entities that + * reference them. + */ + qsort(affected_entities->col_info_list /* what to sort? */, + nfields /* how many? */, + sizeof(struct YbUpdateColInfo) /* how is it stored? */, + YbQsortCompareColRefsList) /* how to sort? */; + + /* Finally create the update matrix */ + YbPopulateUpdateMatrix(affected_entities, nentities, nfields); + + pfree(map); + bms_free(maybe_modified_cols); + list_free(fkeyreflist); + list_free(fkeylist); + + return affected_entities; +} + +/* ---------------------------------------------------------------- + * YbUpdateComputeIndexColumnReferences + * + * Note - At first glance, this function may seem rather similar to + * 'has_applicable_indices' in createplan.c. However, the two functions serve + * orthogonal purposes: + * has_applicable_indices - Primary purpose is to determine if the given update + * query can be executed as a non-distributed transaction. When the columns + * being updated are not part of any index, then the index need not be updated. + * If no index needs to be updated, and all updated rows in the main table are + * part of the same shard/tablet, then the query can be executed as a + * non-distributed transaction (assuming all other conditions are satisfied). + * A side effect of this computation is that we now know which indexes are not + * updated as a part of the query. This is the 'no_update_index_list' which is + * now a part of YbSkippableEntities. + * + * YbUpdateComputeIndexColumnReferences - This function is executed only when + * we know that the query will be executed as a distributed transaction. For + * each index that is NOT a part of the 'no_update_index_list', this function + * computes which columns of the index maybe potentially updated. + * If all the columns of an index are identical in the old and new tuples, the + * update of the index can be skipped. This function does not distinguish + * between the key and non-key columns of an index. + * ---------------------------------------------------------------- + */ +static void +YbUpdateComputeIndexColumnReferences(const Relation rel, + const Bitmapset *maybe_modified_cols, + const AttrToColIdxMap map, + YbUpdateAffectedEntities *affected_entities, + YbSkippableEntities *skip_entities, + int *nentities) +{ + /* + * Add the primary key to the head of the entity list so that it gets + * evaluated first. + */ + if (RelationGetPrimaryKeyIndex(rel) != InvalidOid) + { + Bitmapset *pkbms = + RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + bool pk_maybe_modified = false; + int attnum = -1; + while ((attnum = bms_next_member(pkbms, attnum)) >= 0) + { + if (bms_is_member(attnum, maybe_modified_cols)) + { + YbConsiderColumnAndEntityForUpdate(rel, affected_entities, map, + attnum, rel->rd_pkindex, + SKIP_PRIMARY_KEY, nentities); + pk_maybe_modified = true; + } + } + + if (!pk_maybe_modified) + YbAddEntityToSkipList(SKIP_PRIMARY_KEY, rel->rd_pkindex, + skip_entities); + } + + ListCell *lc = NULL; + foreach (lc, rel->rd_indexlist) + { + Oid index_oid = lfirst_oid(lc); + + /* Avoid checking indexes that we already know to be not updated. */ + if (list_member_oid(skip_entities->index_list, index_oid)) + continue; + + /* Avoid adding the primary key again. */ + if (index_oid == rel->rd_pkindex) + continue; + + Relation indexDesc = RelationIdGetRelation(index_oid); + YbSkippableEntityType etype = SKIP_SECONDARY_INDEX; + bool index_maybe_modified = false; + + const AttrNumber offset = YBGetFirstLowInvalidAttributeNumber(rel); + for (int j = 0; j < indexDesc->rd_index->indnatts; j++) + { + const AttrNumber bms_idx = + indexDesc->rd_index->indkey.values[j] - offset; + if (bms_is_member(bms_idx, maybe_modified_cols)) + { + YbConsiderColumnAndEntityForUpdate(rel, affected_entities, map, + bms_idx, index_oid, etype, + nentities); + index_maybe_modified = true; + } + } + + /* + * If the index contains a predicate or an expression, we need to walk + * over the respective expression trees. It is possible that the + * expression or predicate isn't applicable to this update, but the cost + * of validating that will be prohibitively expensive. + */ + Bitmapset *extraattrs = NULL; + if (indexDesc->rd_indpred) + { + YbComputeIndexExprOrPredicateAttrs( + &extraattrs, indexDesc, Anum_pg_index_indpred, offset); + } + + if (indexDesc->rd_indexprs) + { + YbComputeIndexExprOrPredicateAttrs( + &extraattrs, indexDesc, Anum_pg_index_indexprs, offset); + } + + if (extraattrs) + { + int attnum = -1; + while ((attnum = bms_next_member(extraattrs, attnum)) >= 0) + { + if (bms_is_member(attnum, maybe_modified_cols)) + { + YbConsiderColumnAndEntityForUpdate(rel, affected_entities, + map, attnum, index_oid, + etype, nentities); + index_maybe_modified = true; + } + } + } + + if (!index_maybe_modified) + YbAddEntityToSkipList(etype, index_oid, skip_entities); + + RelationClose(indexDesc); + } +} + +static void +YbUpdateComputeForeignKeyColumnReferences(const Relation rel, + const List *fkeylist, + const Bitmapset *maybe_modified_cols, + const AttrToColIdxMap map, + YbUpdateAffectedEntities *affected_entities, + YbSkippableEntities *skip_entities, + bool is_referencing_rel, + int *nentities) +{ + YbSkippableEntityType etype = + is_referencing_rel ? SKIP_REFERENCING_FKEY : SKIP_REFERENCED_FKEY; + const AttrNumber offset = YBGetFirstLowInvalidAttributeNumber(rel); + ListCell *cell; + foreach (cell, fkeylist) + { + ForeignKeyCacheInfo *fkey = (ForeignKeyCacheInfo *) lfirst(cell); + bool fkey_maybe_modified = false; + for (int idx = 0; idx < fkey->nkeys; idx++) + { + const int bms_idx = + (is_referencing_rel ? fkey->conkey[idx] : fkey->confkey[idx]) - offset; + if (bms_is_member(bms_idx, maybe_modified_cols)) + { + YbConsiderColumnAndEntityForUpdate(rel, affected_entities, map, + bms_idx, fkey->conoid, etype, + nentities); + fkey_maybe_modified = true; + } + } + + if (!fkey_maybe_modified) + YbAddEntityToSkipList(etype, fkey->conoid, skip_entities); + } +} diff --git a/src/postgres/src/backend/utils/adt/ri_triggers.c b/src/postgres/src/backend/utils/adt/ri_triggers.c index 420a20ccba78..7e53d2bc0d02 100644 --- a/src/postgres/src/backend/utils/adt/ri_triggers.c +++ b/src/postgres/src/backend/utils/adt/ri_triggers.c @@ -1333,8 +1333,19 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind) */ bool RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) + TupleTableSlot *oldslot, TupleTableSlot *newslot, + const YbSkippableEntities *yb_skip_entities) { + + /* Check if this trigger is already marked as not needing an update */ + if (yb_skip_entities && yb_skip_entities->referenced_fkey_list && + list_member_oid(yb_skip_entities->referenced_fkey_list, + trigger->tgconstraint)) + { + elog(DEBUG2, "Skipping trigger constraint %s", trigger->tgname); + return false; + } + const RI_ConstraintInfo *riinfo; riinfo = ri_FetchConstraintInfo(trigger, pk_rel, true); @@ -1365,8 +1376,18 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, */ bool RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) + TupleTableSlot *oldslot, TupleTableSlot *newslot, + const YbSkippableEntities *yb_skip_entities) { + /* Check if this trigger is marked as not needing an update */ + if (yb_skip_entities && yb_skip_entities->referencing_fkey_list && + list_member_oid(yb_skip_entities->referencing_fkey_list, + trigger->tgconstraint)) + { + elog(DEBUG2, "Skipping trigger constraint %s", trigger->tgname); + return false; + } + const RI_ConstraintInfo *riinfo; int ri_nullcheck; Datum xminDatum; diff --git a/src/postgres/src/backend/utils/cache/relcache.c b/src/postgres/src/backend/utils/cache/relcache.c index 245d55385783..b14c5dd937d4 100644 --- a/src/postgres/src/backend/utils/cache/relcache.c +++ b/src/postgres/src/backend/utils/cache/relcache.c @@ -6888,6 +6888,70 @@ RelationGetFKeyList(Relation relation) return result; } +/* + * YbRelationGetFKeyReferencedByList -- Gets a list of foreign key infos that + * reference the given relation. + * + * Returns a list of ForeignKeyCacheInfo structs, one per FK that is constrained + * on the given relation. This data is a direct copy of relevant fields from + * pg_constraint. The list items are in no particular order. + */ +List * +YbRelationGetFKeyReferencedByList(Relation relation) +{ + List *result = NIL; + TriggerDesc *trigdesc = relation->trigdesc; + + if (!trigdesc) + return result; + + for (int16 i = 0; i < trigdesc->numtriggers; i++) + { + Trigger trigger = trigdesc->triggers[i]; + HeapTuple htup; + ForeignKeyCacheInfo *info; + + /* + * Consider only referential constraints. Avoid duplication by + * considering only DELETEs. UPDATEs have triggers in both directions + * and could cause duplicates. + */ + if (!TRIGGER_TYPE_MATCHES(trigger.tgtype, TRIGGER_TYPE_ROW, + TRIGGER_TYPE_AFTER, TRIGGER_TYPE_DELETE) || + !trigger.tgconstrrelid) + continue; + + /* Each trigger is a 1:1 mapping to a constraint */ + /* Each constraint may or may not be a foreign key constraint */ + htup = SearchSysCacheCopy1(CONSTROID, + ObjectIdGetDatum(trigger.tgconstraint)); + if (!HeapTupleIsValid(htup)) + elog(ERROR, "cache lookup failed for constraint %u", + trigger.tgconstraint); + + Form_pg_constraint constraint = (Form_pg_constraint) GETSTRUCT(htup); + + Assert(constraint->contype == CONSTRAINT_FOREIGN); + Assert(constraint->confrelid == relation->rd_id); + + info = makeNode(ForeignKeyCacheInfo); + info->conoid = constraint->oid; + info->conrelid = constraint->conrelid; + info->confrelid = constraint->confrelid; + + DeconstructFkConstraintRow(htup, &info->nkeys, info->conkey, + info->confkey, info->conpfeqop, NULL, NULL, + NULL, NULL); + + /* Add FK's node to the result list */ + result = lappend(result, info); + + /* We do not cache the computed information anywhere for now. */ + } + + return result; +} + /* * RelationGetIndexList -- get a list of OIDs of indexes on this relation * @@ -7325,23 +7389,54 @@ RelationGetIndexPredicate(Relation relation) return result; } +/* + * YbComputeIndexExprOrPredicateAttrs -- get a bitmap of index attribute numbers for the + * given index relation. + * + * The result has a bit set for each attribute used anywhere in the index + * definitions of the given index relation. (This includes not only + * simple index keys, but attributes used in expressions and partial-index + * predicates.) + * + * Attribute numbers are offset by FirstLowInvalidHeapAttributeNumber (or its + * YB equivalent) so that we can include system attributes (e.g., OID) in the + * bitmap representation. + * + * The returned result is palloc'd in the caller's memory context and should + * be bms_free'd when not needed anymore. + */ +void +YbComputeIndexExprOrPredicateAttrs(Bitmapset **indexattrs, + Relation indexDesc, + const int Anum_pg_index, + AttrNumber attr_offset) +{ + bool isnull = false; + Datum datum = heap_getattr( + indexDesc->rd_indextuple, Anum_pg_index, GetPgIndexDescriptor(), + &isnull); + + if (isnull) + return; + + Node *indexNode = stringToNode(TextDatumGetCString(datum)); + pull_varattnos_min_attr(indexNode, 1, indexattrs, attr_offset + 1); +} + bool CheckUpdateExprOrPred(const Bitmapset *updated_attrs, Relation indexDesc, const int Anum_pg_index, AttrNumber attr_offset) { - bool isnull = false; - Datum datum = heap_getattr( - indexDesc->rd_indextuple, Anum_pg_index, GetPgIndexDescriptor(), &isnull); - if (isnull) - return false; - - Node *indexNode = stringToNode(TextDatumGetCString(datum)); - Bitmapset *indexattrs = NULL; - pull_varattnos_min_attr(indexNode, 1, &indexattrs, attr_offset + 1); - return bms_overlap(updated_attrs, indexattrs); + Bitmapset *indexattrs = NULL; + YbComputeIndexExprOrPredicateAttrs( + &indexattrs, indexDesc, Anum_pg_index, attr_offset); + bool need_update = bms_overlap(updated_attrs, indexattrs); + bms_free(indexattrs); + return need_update; } + /* * CheckIndexForUpdate -- Given Oid of an Index corresponding to a specific * relation and the set of attributes that are updated because of an SQL diff --git a/src/postgres/src/backend/utils/misc/guc.c b/src/postgres/src/backend/utils/misc/guc.c index 2ae93f03f617..3f055e53f5ad 100644 --- a/src/postgres/src/backend/utils/misc/guc.c +++ b/src/postgres/src/backend/utils/misc/guc.c @@ -4645,6 +4645,31 @@ static struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"yb_update_num_cols_to_compare", PGC_USERSET, CUSTOM_OPTIONS, + gettext_noop("Maximum number of columns whose data is to be" + " compared while seeking to optimize updates." + " If set to 0, all applicable columns in the table" + " will be compared."), + NULL + }, + &yb_update_optimization_options.num_cols_to_compare, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"yb_update_max_cols_size_to_compare", PGC_USERSET, CUSTOM_OPTIONS, + gettext_noop("Maximum size in bytes of columns whose data is to be" + " compared while seeking to optimize updates." + " If set to 0, no size limit is applied."), + NULL, GUC_UNIT_BYTE + }, + &yb_update_optimization_options.max_cols_size_to_compare, + 10 * 1024, 0, INT_MAX, + NULL, NULL, NULL + }, + { {"yb_parallel_range_rows", PGC_USERSET, QUERY_TUNING_OTHER, gettext_noop("The number of rows to plan per parallel worker"), diff --git a/src/postgres/src/backend/utils/misc/pg_yb_utils.c b/src/postgres/src/backend/utils/misc/pg_yb_utils.c index 8207e1689f90..5edb8d4bbeac 100644 --- a/src/postgres/src/backend/utils/misc/pg_yb_utils.c +++ b/src/postgres/src/backend/utils/misc/pg_yb_utils.c @@ -1567,6 +1567,11 @@ bool yb_enable_saop_pushdown = true; int yb_toast_catcache_threshold = -1; int yb_parallel_range_size = 1024 * 1024; +YBUpdateOptimizationOptions yb_update_optimization_options = { + .num_cols_to_compare = 50, + .max_cols_size_to_compare = 10 * 1024 +}; + //------------------------------------------------------------------------------ // YB Debug utils. @@ -5134,6 +5139,15 @@ YbGetRedactedQueryString(const char* query, int query_len, *redacted_query_len = strlen(*redacted_query); } +bool +YbIsUpdateOptimizationEnabled() +{ + /* TODO(kramanathan): Placeholder until a flag strategy is agreed upon */ + return (!YBCIsEnvVarTrue("FLAGS_ysql_skip_row_lock_for_update")) && + yb_update_optimization_options.num_cols_to_compare > 0 && + yb_update_optimization_options.max_cols_size_to_compare > 0; +} + /* * In YB, a "relfilenode" corresponds to a DocDB table. * This function creates a new DocDB table for the given table, diff --git a/src/postgres/src/include/commands/trigger.h b/src/postgres/src/include/commands/trigger.h index 76afb64f63cc..f5b2d2fbe9b4 100644 --- a/src/postgres/src/include/commands/trigger.h +++ b/src/postgres/src/include/commands/trigger.h @@ -269,9 +269,11 @@ extern bool AfterTriggerPendingOnRel(Oid relid); * in utils/adt/ri_triggers.c */ extern bool RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, - TupleTableSlot *old_slot, TupleTableSlot *new_slot); + TupleTableSlot *old_slot, TupleTableSlot *new_slot, + const YbSkippableEntities *yb_skip_entities); extern bool RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, - TupleTableSlot *old_slot, TupleTableSlot *new_slot); + TupleTableSlot *old_slot, TupleTableSlot *new_slot, + const YbSkippableEntities *yb_skip_entities); extern bool RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel); extern void RI_PartitionRemove_Check(Trigger *trigger, Relation fk_rel, diff --git a/src/postgres/src/include/executor/executor.h b/src/postgres/src/include/executor/executor.h index 290fc95cdb2a..6c5a4298226d 100644 --- a/src/postgres/src/include/executor/executor.h +++ b/src/postgres/src/include/executor/executor.h @@ -643,8 +643,7 @@ extern List *ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate, bool update, bool noDupErr, - bool *specConflict, List *arbiterIndexes, - List *no_update_index_list); + bool *specConflict, List *arbiterIndexes); extern bool ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate, ItemPointer conflictTid, @@ -657,10 +656,6 @@ extern void check_exclusion_constraint(Relation heap, Relation index, EState *estate, bool newIndex); extern void ExecDeleteIndexTuples(ResultRelInfo *resultRelInfo, Datum ybctid, HeapTuple tuple, EState *estate); -extern void ExecDeleteIndexTuplesOptimized(ResultRelInfo *resultRelInfo, Datum ybctid, - HeapTuple tuple, EState *estate, - List *no_update_index_list); -extern bool ContainsIndexRelation(Oid indexrelid, List *no_update_index_list); /* * prototypes from functions in execReplication.c diff --git a/src/postgres/src/include/executor/ybOptimizeModifyTable.h b/src/postgres/src/include/executor/ybOptimizeModifyTable.h new file mode 100644 index 000000000000..c4d51fbdcb33 --- /dev/null +++ b/src/postgres/src/include/executor/ybOptimizeModifyTable.h @@ -0,0 +1,37 @@ +/*------------------------------------------------------------------------- + * + * ybOptimizeModifyTable.h + * + * 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. + * + * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/ybOptimizeModifyTable.h + * + *------------------------------------------------------------------------- + */ + +#pragma once + +#include "nodes/execnodes.h" +#include "nodes/plannodes.h" + +extern void YbComputeModifiedColumnsAndSkippableEntities( + ModifyTable *plan, ResultRelInfo *resultRelInfo, EState *estate, + HeapTuple oldtuple, HeapTuple newtuple, Bitmapset **updatedCols, + bool beforeRowUpdateTriggerFired); + +extern bool YbIsPrimaryKeyUpdated(Relation rel, const Bitmapset *updated_cols); diff --git a/src/postgres/src/include/nodes/execnodes.h b/src/postgres/src/include/nodes/execnodes.h index 1d9b3cb7ad23..3a6e6bd4137c 100644 --- a/src/postgres/src/include/nodes/execnodes.h +++ b/src/postgres/src/include/nodes/execnodes.h @@ -727,6 +727,17 @@ typedef struct EState * a SQL statement don't read any value written by the same statement. */ uint64_t yb_es_in_txn_limit_ht_for_reads; + + /* + * A collection of entities (grouped by type) whose bookkeeping updates can + * be skipped. This contains all the skippable entities computed at + * planning time (see ModifyTable in plannodes.h) plus a subset of entities + * in YbUpdateAffectedEntities that are discovered to be skippable at + * execution time. + * Marking this field as a struct rather than a pointer allows us to avoid + * an extra memory allocation per tuple. + */ + YbSkippableEntities yb_skip_entities; } EState; /* @@ -1362,7 +1373,7 @@ typedef struct ModifyTableState double mt_merge_deleted; /* YB specific attributes. */ - bool yb_fetch_target_tuple; /* Perform initial scan to populate + bool yb_fetch_target_tuple; /* Perform initial scan to populate * the ybctid. */ } ModifyTableState; diff --git a/src/postgres/src/include/nodes/nodes.h b/src/postgres/src/include/nodes/nodes.h index 2de46c5182f6..d4893051737e 100644 --- a/src/postgres/src/include/nodes/nodes.h +++ b/src/postgres/src/include/nodes/nodes.h @@ -559,6 +559,8 @@ typedef enum NodeTag T_YbCreateProfileStmt, T_YbDropProfileStmt, T_YbTIDBitmap, + T_YbSkippableEntities, + T_YbUpdateAffectedEntities, } NodeTag; diff --git a/src/postgres/src/include/nodes/plannodes.h b/src/postgres/src/include/nodes/plannodes.h index 880d08d9eacf..07f439a0b0b8 100644 --- a/src/postgres/src/include/nodes/plannodes.h +++ b/src/postgres/src/include/nodes/plannodes.h @@ -23,6 +23,7 @@ #include "nodes/parsenodes.h" #include "nodes/pathnodes.h" #include "nodes/primnodes.h" +#include "nodes/ybbitmatrix.h" /* ---------------------------------------------------------------- @@ -210,6 +211,117 @@ typedef struct ProjectSet Plan plan; } ProjectSet; +/* + * An enum whose values describe the type of entity that may be excluded from + * bookkeeping operations performed by an UPDATE or an INSERT ON CONFLICT DO + * UPDATE query. + */ +typedef enum +{ + SKIP_PRIMARY_KEY, + SKIP_SECONDARY_INDEX, + SKIP_REFERENCING_FKEY, + SKIP_REFERENCED_FKEY, +} YbSkippableEntityType; + +/* + * YbSkippableEntities is a collection of skip lists. Each skip list contains + * entities of a specific type (YbSkippableEntityType). The elements of the skip + * lists can be excluded from bookkeeping operations when being updated - for + * example entities in the index_list need not be updated, constraints in the + * ref*_fkey_list(s) need not be checked and so on). + */ +typedef struct +{ + NodeTag type; + + /* A list of indexes whose update can be skipped */ + List *index_list; + + /* + * A list of skippable foreign key relationships where the relation under + * consideration is the referencing relation. + */ + List *referencing_fkey_list; + + /* + * A list of skippable foreign key relationships where the relation under + * consideration is the referenced relation. + * + * NOTE - The disambiugation between referencing and referenced foreign keys + * relationships is important in the context of self-referential foreign key + * constraints where one side of the relationship may be skippable, and the + * other side may not be. + */ + List *referenced_fkey_list; +} YbSkippableEntities; + +/* + * YbUpdateAffectedEntities holds planning time computation of entities (index, + * constraints, etc) that maybe affected by a ModifyTable query (currently used + * by only UPDATE or an INSERT ON CONFLICT DO UPDATE queries). + * Information held in this structure includes a list of columns that may be + * modified by the query, a list of entities that reference these columns, and + * the mapping between them. + */ +typedef struct YbUpdateAffectedEntities +{ + NodeTag type; + /* + * Identifying information about a list of entities that may be impacted by + * the current query. + */ + struct YbUpdateEntity { + Oid oid; /* OID of the entity that might need an update */ + YbSkippableEntityType etype; /* What type of entity is it? */ + } *entity_list; + + /* + * YbUpdateColInfo holds information about columns that may be modified as + * part of an UPDATE or an INSERT ON CONFLICT DO UPDATE query. + * This includes - + * 1. The attribute number of the column + * 2. A list of entities that reference the column + * + * An entity is said to reference a column in a tuple if modification of the + * column in the tuple could result in a housekeeping operation to be + * performed on the entity. Examples include indexes and constraints. + * Modification of one of the columns in the index will cause an index + * update operation. Similarly, modification of a column that has a foreign + * key constraint on it will trigger a referential integrity check. + */ + struct YbUpdateColInfo { + /* + * The attribute number of the column offset by + *'YBFirstLowInvalidAttributeNumber' or 'FirstLowInvalidHeapAttributeNumber' + * The offset makes the attribute number non-negative, allowing direct + * interfacing with bitmapsets. + */ + AttrNumber attnum; + + /* + * A list of entities that reference the column. + * Entities are identified by their index in the entity_list. + */ + List *entity_refs; + } *col_info_list; + + /* + * A matrix that facilitates optimizing the number of columns to be compared + * to decide if an entity is affected by the query. The columns of the + * matrix represent the entities under consideration, the rows of the matrix + * represent the columns/fields in the relation affected by the query. + * A working copy of this matrix is made for each tuple that is updated at + * execution time. + */ + YbBitMatrix matrix; +} YbUpdateAffectedEntities; + +#define YB_UPDATE_AFFECTED_ENTITIES_NUM_FIELDS(state) \ + YbBitMatrixNumRows(&state->matrix) +#define YB_UPDATE_AFFECTED_ENTITIES_NUM_ENTITIES(state) \ + YbBitMatrixNumCols(&state->matrix) + /* ---------------- * ModifyTable node - * Apply rows produced by outer plan to result table(s), @@ -255,9 +367,27 @@ typedef struct ModifyTable List *ybReturningColumns; /* columns to fetch from DocDB */ List *ybColumnRefs; /* colrefs to evaluate pushdown expressions */ bool no_row_trigger; /* planner has checked no triggers apply */ - List *no_update_index_list; /* OIDs of indexes to be aren't updated */ bool ybUseScanTupleInUpdate; /* use old scan tuple in UPDATE to construct the new tuple */ bool ybHasWholeRowAttribute; /* whether subplan tlist contains wholerow junk attribute */ + + /* + * A collection of entities that are impacted by the ModifyTable query, and + * their relationship to the columns that are modified by the query. This is + * currently used only by UPDATE and INSERT ON CONFLICT DO UPDATE queries. + * The contents of this struct are computed at planning time and remain + * immutable for the lifetime of the plan. + */ + YbUpdateAffectedEntities *yb_update_affected_entities; + + /* + * A collection of entity OIDs (grouped by type) for which it is known at + * planning time that bookkeeping updates can be skipped. + * This is currently used only by UPDATE and INSERT ON CONFLICT DO UPDATE + * queries. + * The entities in this struct are mutually exclusive to the entities in + * yb_update_affected_entities. + */ + YbSkippableEntities *yb_skip_entities; } ModifyTable; struct PartitionPruneInfo; /* forward reference to struct below */ diff --git a/src/postgres/src/include/nodes/ybbitmatrix.h b/src/postgres/src/include/nodes/ybbitmatrix.h new file mode 100644 index 000000000000..d46f89981da7 --- /dev/null +++ b/src/postgres/src/include/nodes/ybbitmatrix.h @@ -0,0 +1,54 @@ +/*------------------------------------------------------------------------- + * + * ybbitmatrix.h + * Yugabyte boolean bitmap matrix package + * + * This module provides a boolean matrix data structure that is internally + * implemented as a bitmapset (nodes/bitmapset.h). This allows for efficient + * storage and row-level operations. + * + * + * Copyright (c) 2003-2018, PostgreSQL Global Development Group + * + * src/include/nodes/ybtidbitmatrix.h + * + *------------------------------------------------------------------------- + */ +#pragma once + +#include "nodes/bitmapset.h" + +/* + * YbBitMatrix is a bitmapset implementation of a generic two-dimensional + * matrix that is composed of (nrows * ncols) of boolean data. + */ +typedef struct +{ + int nrows; /* Number of rows in the matrix */ + int ncols; /* Number of columns in the matrix */ + + /* The data held in the matrix implemented as a one-dimensional BMS */ + Bitmapset *data; +} YbBitMatrix; + +/* Lifecycle operations */ +extern void YbInitBitMatrix(YbBitMatrix *matrix, int nrows, int ncols); +extern void YbCopyBitMatrix(YbBitMatrix *dest, const YbBitMatrix *src); +extern void YbFreeBitMatrix(YbBitMatrix *matrix); + +/* General operations */ +extern int YbBitMatrixNumRows(const YbBitMatrix *matrix); +extern int YbBitMatrixNumCols(const YbBitMatrix *matrix); + +/* Row-level operations */ +extern void YbBitMatrixSetRow(YbBitMatrix *matrix, int row_idx, bool value); +extern int YbBitMatrixNextMemberInColumn(const YbBitMatrix *matrix, int col_idx, + int prev_row); +extern int YbBitMatrixNextMemberInRow(const YbBitMatrix *matrix, int row_idx, + int prev_col); + +/* Individual cell-level operations */ +extern bool YbBitMatrixGetValue(const YbBitMatrix *matrix, int row_idx, + int col_idx); +extern void YbBitMatrixSetValue(YbBitMatrix *matrix, int row_idx, int col_idx, + bool value); diff --git a/src/postgres/src/include/optimizer/ybcplan.h b/src/postgres/src/include/optimizer/ybcplan.h index dfa323f30260..5d8f88dc8b87 100644 --- a/src/postgres/src/include/optimizer/ybcplan.h +++ b/src/postgres/src/include/optimizer/ybcplan.h @@ -45,3 +45,17 @@ void extract_pushdown_clauses(List *restrictinfo_list, List **rel_colrefs, List **idx_remote_quals, List **idx_colrefs); + +/* YbSkippableEntities helper functions*/ +extern YbSkippableEntities *YbInitSkippableEntities(List *no_update_index_list); +extern void YbCopySkippableEntities(YbSkippableEntities *dst, + const YbSkippableEntities *src); +extern void YbAddEntityToSkipList(YbSkippableEntityType etype, Oid oid, + YbSkippableEntities *skip_entities); +extern void YbClearSkippableEntities(YbSkippableEntities *skip_entities); + + +extern struct YbUpdateAffectedEntities * +YbComputeAffectedEntitiesForRelation(ModifyTable *modifyTable, + const Relation rel, + Bitmapset *update_attrs); diff --git a/src/postgres/src/include/pg_yb_utils.h b/src/postgres/src/include/pg_yb_utils.h index 0ad62ab10386..b003979fe5f7 100644 --- a/src/postgres/src/include/pg_yb_utils.h +++ b/src/postgres/src/include/pg_yb_utils.h @@ -38,6 +38,7 @@ #include "utils/guc.h" #include "utils/relcache.h" #include "utils/resowner.h" +#include "utils/typcache.h" #include "yb/yql/pggate/util/ybc_util.h" #include "yb/yql/pggate/ybc_pggate.h" @@ -557,6 +558,8 @@ extern bool yb_prefer_bnl; */ extern bool yb_explain_hide_non_deterministic_fields; +extern int yb_update_num_cols_to_compare; +extern int yb_update_max_cols_size_to_compare; /* * Enables scalar array operation pushdown. * If true, planner sends supported expressions to DocDB for evaluation @@ -671,6 +674,15 @@ YbDdlRollbackEnabled () { extern bool yb_use_hash_splitting_by_default; +typedef struct YBUpdateOptimizationOptions +{ + int num_cols_to_compare; + int max_cols_size_to_compare; +} YBUpdateOptimizationOptions; + +/* GUC variables to control the behavior of optimizing update queries. */ +extern YBUpdateOptimizationOptions yb_update_optimization_options; + /* * GUC to allow user to silence the error saying that advisory locks are not * supported. @@ -1160,6 +1172,9 @@ extern SortByDir YbSortOrdering(SortByDir ordering, bool is_colocated, bool is_t extern void YbGetRedactedQueryString(const char* query, int query_len, const char** redacted_query, int* redacted_query_len); +/* Check if optimizations for UPDATE queries have been enabled. */ +extern bool YbIsUpdateOptimizationEnabled(); + extern void YbRelationSetNewRelfileNode(Relation rel, Oid relfileNodeId, bool yb_copy_split_options, bool is_truncate); diff --git a/src/postgres/src/include/utils/relcache.h b/src/postgres/src/include/utils/relcache.h index d9c30379378f..a6d25e4b7978 100644 --- a/src/postgres/src/include/utils/relcache.h +++ b/src/postgres/src/include/utils/relcache.h @@ -52,6 +52,7 @@ extern List *RelationGetDummyIndexExpressions(Relation relation); extern List *RelationGetIndexPredicate(Relation relation); extern Datum *RelationGetIndexRawAttOptions(Relation relation); extern bytea **RelationGetIndexAttOptions(Relation relation, bool copy); +extern List *YbRelationGetFKeyReferencedByList(Relation relation); typedef enum IndexAttrBitmapKind { @@ -63,6 +64,10 @@ typedef enum IndexAttrBitmapKind extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind); +extern void YbComputeIndexExprOrPredicateAttrs(Bitmapset **indexattrs, + Relation indexDesc, + const int Anum_pg_index, + AttrNumber attr_offset); extern Bitmapset *RelationGetIdentityKeyBitmap(Relation relation); diff --git a/src/postgres/src/test/regress/expected/yb_update_optimize_base.out b/src/postgres/src/test/regress/expected/yb_update_optimize_base.out new file mode 100644 index 000000000000..f620631f7d35 --- /dev/null +++ b/src/postgres/src/test/regress/expected/yb_update_optimize_base.out @@ -0,0 +1,1393 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. +-- CREATE functions that can be triggered upon update to modify various columns +CREATE OR REPLACE FUNCTION no_update() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + RAISE NOTICE 'Trigger "no_update" invoked'; + RETURN NEW; +END; +$$; +CREATE OR REPLACE FUNCTION increment_v1() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.v1 = NEW.v1 + 1; + RETURN NEW; +END; +$$; +CREATE OR REPLACE FUNCTION increment_v3() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.v3 = NEW.v3 + 1; + RETURN NEW; +END; +$$; +CREATE OR REPLACE FUNCTION update_all() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.h = NEW.h + 1024; + NEW.v1 = NEW.v1 + 1024; + NEW.v2 = NEW.v2 + 1; + RETURN NEW; +END; +$$; +-- CREATE a simple table with only a primary key +DROP TABLE IF EXISTS pkey_only_table; +NOTICE: table "pkey_only_table" does not exist, skipping +CREATE TABLE pkey_only_table (h INT PRIMARY KEY, v INT); +INSERT INTO pkey_only_table (SELECT i, i FROM generate_series(1, 1024) AS i); +-- A simple point update without involving the primary key +-- This query does not go through the distributed transaction path +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v = v + 1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +-- Point updates that include the primary key in the targetlist +-- These queries do not go through the distributed transaction path either +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = v - 1, v = v + 1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Index Scan using pkey_only_table_pkey on pkey_only_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h, v = v + 1 WHERE h = 1; -- Needs further optimization +ERROR: Missing column ybctid in DELETE request +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = 1, v = v + 1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Index Scan using pkey_only_table_pkey on pkey_only_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Queries affecting a range of rows +-- Since the primary key is not specified in its entirety, these use the distributed txns +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h, v = v + 1 WHERE h > 10 AND h < 15; + QUERY PLAN +----------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Seq Scan on pkey_only_table (actual rows=4 loops=1) + Storage Filter: ((h > 10) AND (h < 15)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = v, v = v + 1 WHERE h > 20 AND h < 25; + QUERY PLAN +----------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Seq Scan on pkey_only_table (actual rows=4 loops=1) + Storage Filter: ((h > 20) AND (h < 25)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +-- Query that updates the primary key. This should involve multiple flushes +-- over a distributed transaction. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h + 1024, v = v + 1 WHERE h < 5; + QUERY PLAN +----------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Seq Scan on pkey_only_table (actual rows=4 loops=1) + Storage Filter: (h < 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 8 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 8 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h + 1024, v = v + 1 WHERE h < 2000; + QUERY PLAN +-------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Seq Scan on pkey_only_table (actual rows=1024 loops=1) + Storage Filter: (h < 2000) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 2048 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 2048 + Storage Flush Requests: 1 +(10 rows) + +DROP TABLE pkey_only_table; +-- CREATE a table with no primary key, but having a secondary indexes. +DROP TABLE IF EXISTS secindex_only_table; +NOTICE: table "secindex_only_table" does not exist, skipping +CREATE TABLE secindex_only_table (v1 INT, v2 INT, v3 INT); +INSERT INTO secindex_only_table (SELECT i, i, i FROM generate_series(1, 1024) AS i); +-- Add an index on v1 +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1 ON secindex_only_table (v1); +-- Updates not involving the secondary index should not have multiple flushes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v3 = v2 + 1 WHERE v1 = 1; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +-- This specifically tests the case where there is no overlap between the index +-- and columns that are potentially modified. The index should be added to a +-- skip list without further consideration +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v2 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Point updates that include the column referenced by the index in the targetlist. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v1 = 1; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v2, v3 = v3 + 1 WHERE v1 = 1; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +-- Queries affecting a range of rows +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v1 < 5; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=4 loops=1) + Storage Filter: (v1 < 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v2, v3 = v3 + 1 WHERE v1 < 5; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=4 loops=1) + Storage Filter: (v1 < 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v2 < 5; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=4 loops=1) + Storage Filter: (v2 < 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +-- Special case where no column is affected. We should still see a flush in order +-- to acquire necessary row lock on the main table. This is similar to SELECT FOR UPDATEs. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 = 5; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 < 5; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=4 loops=1) + Storage Filter: (v1 < 5) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 4 + Storage Flush Requests: 1 +(10 rows) + +-- Add a second index to the table which is a multi-column index +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1_v2 ON secindex_only_table (v1, v2); +-- Queries that affect only one of the two indexes +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v2 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Same as above but queries that actually modify one of the indexes exclusively. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 15; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 15) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(13 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v2 = 16; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=2 loops=1) + Storage Filter: (v2 = 16) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 2 + Storage Index Write Requests: 4 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 6 + Storage Flush Requests: 1 +(11 rows) + +-- Queries that cover both indexes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 WHERE v1 = 15; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 15) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 15; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 15) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(13 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 15; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 15) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(13 rows) + +-- Add a simple trigger to the table. +CREATE TRIGGER secindex_only_table_no_update BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION no_update(); +-- Repeat the above queries to validate that the number of flushes do not change. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(13 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v2 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 25; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 25) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(14 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 25; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 25) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(14 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 25; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 25) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(14 rows) + +-- Add a trigger that modifies v3 (no indexes on it) +CREATE TRIGGER secindex_only_table_increment_v3 BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION increment_v3(); +-- Read the values corresponding to v1 = 1. +SELECT * FROM secindex_only_table WHERE v1 = 1; + v1 | v2 | v3 +----+----+---- + 1 | 1 | 9 +(1 row) + +-- Repeat a subset of the above queries to validate that no extra flushes are required. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(14 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 35; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 35) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(15 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 35; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 35) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(15 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 35; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 35) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(15 rows) + +-- Read the values corresponding to v1 = 1 again to validate that v3 is incremented twice: +-- once by the query, once by the trigger. +SELECT * FROM secindex_only_table WHERE v1 = 1; + v1 | v2 | v3 +----+----+---- + 1 | 1 | 11 +(1 row) + +-- Add a trigger that modifies v1 (has two indexes on it) +CREATE TRIGGER secindex_only_table_increment_v1 BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION increment_v1(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_increment_v1: calls=1 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(16 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v2 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_increment_v1: calls=1 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(14 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 45; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 45) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_increment_v1: calls=1 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(16 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 55; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Index Scan using secindex_only_table_v1 on secindex_only_table (actual rows=1 loops=1) + Index Cond: (v1 = 55) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger secindex_only_table_increment_v1: calls=1 + Trigger secindex_only_table_increment_v3: calls=1 + Trigger secindex_only_table_no_update: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(16 rows) + +-- Read the values to confirm that there is/are: +-- No row corresponding to v1 = 45 +-- Two rows for v1 = 46 +-- One row for v1 = 47 +-- No rows for v1 = 55 +-- One row for v1 = 46 +-- Two rows for v1 = 47 +SELECT * FROM secindex_only_table WHERE v1 IN (45, 46, 47, 55, 56, 57) ORDER BY v1, v2; + v1 | v2 | v3 +----+----+---- + 46 | 46 | 46 + 46 | 46 | 46 + 47 | 47 | 47 + 56 | 56 | 56 + 57 | 56 | 56 + 57 | 57 | 57 +(6 rows) + +-- Query that updates a range of values between 61 and 70. +-- TODO: Validate this +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 > 60 AND v1 <= 70; +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=10 loops=1) + Storage Filter: ((v1 > 60) AND (v1 <= 70)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 10 + Storage Index Write Requests: 40 + Storage Flush Requests: 9 + Trigger secindex_only_table_increment_v1: calls=10 + Trigger secindex_only_table_increment_v3: calls=10 + Trigger secindex_only_table_no_update: calls=10 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 50 + Storage Flush Requests: 10 +(15 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v3 = v3 + 1 WHERE v1 > 70 AND v1 <= 80; +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked +NOTICE: Trigger "no_update" invoked + QUERY PLAN +---------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=11 loops=1) + Storage Filter: ((v1 > 70) AND (v1 <= 80)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 11 + Storage Index Write Requests: 44 + Storage Flush Requests: 10 + Trigger secindex_only_table_increment_v1: calls=11 + Trigger secindex_only_table_increment_v3: calls=11 + Trigger secindex_only_table_no_update: calls=11 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 55 + Storage Flush Requests: 11 +(15 rows) + +DROP TABLE secindex_only_table; +-- CREATE a table with no primary key or secondary indexes. +DROP TABLE IF EXISTS no_index_table; +NOTICE: table "no_index_table" does not exist, skipping +CREATE TABLE no_index_table (h INT, v INT); +INSERT INTO no_index_table (SELECT i, i FROM generate_series(1, 1024) AS i); +-- Point update queries. Irrespective of whether the columns in the targetlist +-- are modified, we should see flushes updating the row. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h, v = v WHERE h = 1; + QUERY PLAN +---------------------------------------------------------- + Update on no_index_table (actual rows=0 loops=1) + -> Seq Scan on no_index_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h WHERE h = 1; + QUERY PLAN +---------------------------------------------------------- + Update on no_index_table (actual rows=0 loops=1) + -> Seq Scan on no_index_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h + 1 WHERE h = 10; + QUERY PLAN +---------------------------------------------------------- + Update on no_index_table (actual rows=0 loops=1) + -> Seq Scan on no_index_table (actual rows=1 loops=1) + Storage Filter: (h = 10) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET v = v + 1 WHERE v = 10; + QUERY PLAN +---------------------------------------------------------- + Update on no_index_table (actual rows=0 loops=1) + -> Seq Scan on no_index_table (actual rows=1 loops=1) + Storage Filter: (v = 10) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = v + 1, v = h + 1 WHERE v = 20; + QUERY PLAN +---------------------------------------------------------- + Update on no_index_table (actual rows=0 loops=1) + -> Seq Scan on no_index_table (actual rows=1 loops=1) + Storage Filter: (v = 20) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1024 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1024 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- CREATE a hierarchy of tables of the order a <-- b <-- c where 'a' is the base +-- table, 'b' references 'a' and is a reference to 'c', and so on. +DROP TABLE IF EXISTS a_test; +NOTICE: table "a_test" does not exist, skipping +DROP TABLE IF EXISTS b1_test; +NOTICE: table "b1_test" does not exist, skipping +DROP TABLE IF EXISTS b2_test; +NOTICE: table "b2_test" does not exist, skipping +DROP TABLE IF EXISTS c1_test; +NOTICE: table "c1_test" does not exist, skipping +DROP TABLE IF EXISTS c2_test; +NOTICE: table "c2_test" does not exist, skipping +CREATE TABLE a_test (h INT PRIMARY KEY, v1 INT UNIQUE, v2 INT); +CREATE INDEX NONCONCURRENTLY a_v1 ON a_test (v1); +CREATE TABLE b1_test (h INT PRIMARY KEY REFERENCES a_test (h), v1 INT UNIQUE REFERENCES a_test (v1), v2 INT); +CREATE INDEX NONCONCURRENTLY b1_v1 ON b1_test (v1); +CREATE TABLE b2_test (h INT PRIMARY KEY REFERENCES a_test (v1), v1 INT REFERENCES a_test (h), v2 INT); +CREATE INDEX NONCONCURRENTLY b2_v1 ON b2_test (v1); +INSERT INTO a_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); +INSERT INTO b1_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); +INSERT INTO b2_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); +-- Point update queries on table 'a'. Should skip 'referenced-by' constraints on +-- b-level tables. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h, v1 = h, v2 = v2 + 1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------- + Update on a_test (actual rows=0 loops=1) + -> Index Scan using a_test_pkey on a_test (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE h = 1; + QUERY PLAN +------------------------------------------ + Update on a_test (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE v1 = 1; + QUERY PLAN +------------------------------------------------------------------------ + Update on a_test (actual rows=0 loops=1) + -> Index Scan using a_test_v1_key on a_test (actual rows=1 loops=1) + Index Cond: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Index Read Requests: 1 + Storage Index Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 2 + Storage Rows Scanned: 2 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE v1 = h AND h = 2; + QUERY PLAN +---------------------------------------------------------------------- + Update on a_test (actual rows=0 loops=1) + -> Index Scan using a_test_pkey on a_test (actual rows=1 loops=1) + Index Cond: (h = 2) + Storage Filter: (v1 = 2) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +-- Same as above but update the rows to validate 'referenced-by' constraints. +INSERT INTO a_test (SELECT i, i, i FROM generate_series(1025, 1034) AS i); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h, v1 = v1 + 1 WHERE h = 1034; + QUERY PLAN +---------------------------------------------------------------------- + Update on a_test (actual rows=0 loops=1) + -> Index Scan using a_test_pkey on a_test (actual rows=1 loops=1) + Index Cond: (h = 1034) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger for constraint b1_test_v1_fkey: calls=1 + Trigger for constraint b2_test_h_fkey: calls=1 + Storage Read Requests: 5 + Storage Rows Scanned: 1 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(13 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h + 1, v1 = v1 + 1 WHERE h = 1034; + QUERY PLAN +---------------------------------------------------------------------- + Update on a_test (actual rows=0 loops=1) + -> Index Scan using a_test_pkey on a_test (actual rows=1 loops=1) + Index Cond: (h = 1034) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Index Write Requests: 4 + Trigger for constraint b1_test_h_fkey: calls=1 + Trigger for constraint b1_test_v1_fkey: calls=1 + Trigger for constraint b2_test_h_fkey: calls=1 + Trigger for constraint b2_test_v1_fkey: calls=1 + Storage Read Requests: 9 + Storage Rows Scanned: 1 + Storage Write Requests: 6 + Storage Flush Requests: 1 +(15 rows) + +-- +-- The following sections tests updates when the update optimization is turned +-- off. This is to prevent regressions. +-- +SET yb_update_num_cols_to_compare TO 0; +DROP TABLE IF EXISTS base_table1; +NOTICE: table "base_table1" does not exist, skipping +CREATE TABLE base_table1 (k INT PRIMARY KEY, v1 INT, v2 INT); +INSERT INTO base_table1 (SELECT i, i, i FROM generate_series(1, 10) AS i); +-- This query below should follow the transaction fast path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +----------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +-- The queries below should use the distributed transaction path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = v1 - 1, v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Flush Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 2 + Storage Flush Requests: 2 +(11 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Flush Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 2 + Storage Flush Requests: 2 +(11 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = k, v1 = v1 + 1 WHERE k > 5 AND k < 15; + QUERY PLAN +------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Seq Scan on base_table1 (actual rows=5 loops=1) + Storage Filter: ((k > 5) AND (k < 15)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10 + Storage Table Write Requests: 10 + Storage Flush Requests: 5 + Storage Read Requests: 1 + Storage Rows Scanned: 10 + Storage Write Requests: 10 + Storage Flush Requests: 6 +(11 rows) + +-- Adding a trigger should automatically make the table use the distributed +-- transaction path. +CREATE TRIGGER base_table1_no_update BEFORE UPDATE ON base_table1 FOR EACH ROW EXECUTE FUNCTION no_update(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; +NOTICE: Trigger "no_update" invoked + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger base_table1_no_update: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +DROP TRIGGER base_table1_no_update ON base_table1; +-- Add a trigger that updates values of v1. +CREATE TRIGGER base_table1_increment_v1 BEFORE UPDATE ON base_table1 FOR EACH ROW EXECUTE FUNCTION increment_v1(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v2 = v2 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger base_table1_increment_v1: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +-- v1 should be updated twice +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger base_table1_increment_v1: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +DROP TRIGGER base_table1_increment_v1 ON base_table1; +-- Drop the trigger and adding a secondary index should produce the same result. +CREATE INDEX NONCONCURRENTLY base_table1_v1 ON base_table1 (v1); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Index Write Requests: 2 + Storage Flush Requests: 2 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 4 + Storage Flush Requests: 3 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(11 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Storage Flush Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 3 + Storage Flush Requests: 2 +(12 rows) + +-- Validate that the values have indeed been updated. +SELECT * FROM base_table1 WHERE k = 1; + k | v1 | v2 +---+----+---- + 1 | 8 | 4 +(1 row) + +-- Use an index-only scan to validate that the index has been updated. +SELECT v1 FROM base_table1 WHERE k = 1; + v1 +---- + 8 +(1 row) + +DROP TABLE base_table1; +-- Turn the GUC back on for the remaining tests +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +-- +-- The following section contains a set of sanity tests for colocated tables/databases. +-- +CREATE DATABASE codb colocation = true; +\c codb +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +DROP TABLE IF EXISTS base_table1; +NOTICE: table "base_table1" does not exist, skipping +CREATE TABLE base_table1 (k INT PRIMARY KEY, v1 INT, v2 INT) WITH (colocation = true); +INSERT INTO base_table1 (SELECT i, i, i FROM generate_series(1, 10) AS i); +-- This query below should follow the transaction fast path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +----------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +-- The queries below should use the distributed transaction path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = v1 - 1, v1 = v1 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=1 loops=1) + Index Cond: (k = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = k, v1 = v1 + 1 WHERE k > 5 AND k < 15; + QUERY PLAN +-------------------------------------------------------------------------------- + Update on base_table1 (actual rows=0 loops=1) + -> Index Scan using base_table1_pkey on base_table1 (actual rows=5 loops=1) + Index Cond: ((k > 5) AND (k < 15)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 5 + Storage Table Write Requests: 5 + Storage Read Requests: 1 + Storage Rows Scanned: 5 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(10 rows) + +DROP TABLE base_table1; +\c yugabyte +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +-- The queries below test self-referential foreign keys +DROP TABLE IF EXISTS ancestry; +NOTICE: table "ancestry" does not exist, skipping +CREATE TABLE ancestry (key TEXT PRIMARY KEY, value TEXT, parent TEXT, gparent TEXT); +ALTER TABLE ancestry ADD CONSTRAINT fk_self_parent_to_key FOREIGN KEY (parent) REFERENCES ancestry (key); +ALTER TABLE ancestry ADD CONSTRAINT fk_self_gparent_to_key FOREIGN KEY (gparent) REFERENCES ancestry (key); +-- Insert the family tree +INSERT INTO ancestry VALUES ('root', 'Dave', NULL, NULL); +INSERT INTO ancestry VALUES ('ggparent1', 'Adam', 'root', NULL); +INSERT INTO ancestry VALUES ('ggparent2', 'Barry', 'root', NULL); +INSERT INTO ancestry VALUES ('gparent1', 'Claudia', 'ggparent1', 'root'); +INSERT INTO ancestry VALUES ('gparent2', 'Donatello', 'ggparent1', 'root'); +INSERT INTO ancestry VALUES ('gparent3', 'Eddie', 'ggparent2', 'root'); +INSERT INTO ancestry VALUES ('parent1', 'Farooq', 'gparent1', 'ggparent1'); +INSERT INTO ancestry VALUES ('parent2', 'Govind', 'gparent1', 'ggparent2'); +INSERT INTO ancestry VALUES ('child1', 'Harry', 'parent1', 'gparent1'); +INSERT INTO ancestry VALUES ('child2', 'Ivan', 'parent2', 'gparent1'); +-- Query modifying the value column should not trigger referential constraint check. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET value = 'David' WHERE value = 'Dave'; + QUERY PLAN +---------------------------------------------------- + Update on ancestry (actual rows=0 loops=1) + -> Seq Scan on ancestry (actual rows=1 loops=1) + Storage Filter: (value = 'Dave'::text) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'root', value = 'David' WHERE key = 'root'; + QUERY PLAN +-------------------------------------------------------------------------- + Update on ancestry (actual rows=0 loops=1) + -> Index Scan using ancestry_pkey on ancestry (actual rows=1 loops=1) + Index Cond: (key = 'root'::text) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Query modifying one's parent should trigger trigger the referential +-- constraint in only one direction to see if the parent exists. Grandparent +-- checks in both directions should be skipped. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET parent = 'ggparent2', gparent = 'root' WHERE value = 'Donatello'; + QUERY PLAN +------------------------------------------------------- + Update on ancestry (actual rows=0 loops=1) + -> Seq Scan on ancestry (actual rows=1 loops=1) + Storage Filter: (value = 'Donatello'::text) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10 + Storage Table Write Requests: 1 + Trigger for constraint fk_self_parent_to_key: calls=1 + Storage Read Requests: 2 + Storage Rows Scanned: 11 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +-- This query should throw an error as the parent doesn't exist +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET parent = 'ggparent23', gparent = 'root' WHERE key = 'gparent2'; +ERROR: insert or update on table "ancestry" violates foreign key constraint "fk_self_parent_to_key" +DETAIL: Key (parent)=(ggparent23) is not present in table "ancestry". +-- Query modifying the key to the row should trigger checks in only the children +-- and grandchildren. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'gparent30' WHERE value = 'Eddie'; + QUERY PLAN +-------------------------------------------------------- + Update on ancestry (actual rows=0 loops=1) + -> Seq Scan on ancestry (actual rows=1 loops=1) + Storage Filter: (value = 'Eddie'::text) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10 + Storage Table Write Requests: 2 + Trigger for constraint fk_self_parent_to_key: calls=1 + Trigger for constraint fk_self_gparent_to_key: calls=1 + Storage Read Requests: 5 + Storage Rows Scanned: 30 + Storage Write Requests: 2 + Storage Flush Requests: 1 +(12 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'gparent3' WHERE key = 'gparent30'; + QUERY PLAN +-------------------------------------------------------------------------- + Update on ancestry (actual rows=0 loops=1) + -> Index Scan using ancestry_pkey on ancestry (actual rows=1 loops=1) + Index Cond: (key = 'gparent30'::text) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Trigger for constraint fk_self_parent_to_key: calls=1 + Trigger for constraint fk_self_gparent_to_key: calls=1 + Storage Read Requests: 5 + Storage Rows Scanned: 21 + Storage Write Requests: 2 + Storage Flush Requests: 1 +(12 rows) diff --git a/src/postgres/src/test/regress/expected/yb_update_optimize_indices.out b/src/postgres/src/test/regress/expected/yb_update_optimize_indices.out new file mode 100644 index 000000000000..ce79d961804d --- /dev/null +++ b/src/postgres/src/test/regress/expected/yb_update_optimize_indices.out @@ -0,0 +1,523 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. +-- CREATE a table with a primary key and no secondary indexes +DROP TABLE IF EXISTS pkey_only_table; +NOTICE: table "pkey_only_table" does not exist, skipping +CREATE TABLE pkey_only_table (h INT PRIMARY KEY, v1 INT, v2 INT); +INSERT INTO pkey_only_table (SELECT i, i, i FROM generate_series(1, 10240) AS i); +EXPLAIN (ANALYZE, DIST, COSTS OFF) SELECT * FROM pkey_only_table; + QUERY PLAN +--------------------------------------------------------- + Seq Scan on pkey_only_table (actual rows=10240 loops=1) + Storage Table Read Requests: 12 + Storage Table Rows Scanned: 10240 + Storage Read Requests: 12 + Storage Rows Scanned: 10240 + Storage Write Requests: 0 + Storage Flush Requests: 0 +(7 rows) + +-- Updating non-index column should be done in a single op +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, v2 = 1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 2 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 3, v2 = 3 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = v1 + 1, v2 = v1 + 1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Result (actual rows=1 loops=1) + Storage Table Write Requests: 1 + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 1 + Storage Flush Requests: 0 +(7 rows) + +-- Setting index column in the update should trigger a read, but no update of the index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = 1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Index Scan using pkey_only_table_pkey on pkey_only_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = 1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Index Scan using pkey_only_table_pkey on pkey_only_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- TODO: Fix bug here +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = h WHERE h = 1; +ERROR: Missing column ybctid in DELETE request +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = v1 WHERE h = 1; + QUERY PLAN +---------------------------------------------------------------------------------------- + Update on pkey_only_table (actual rows=0 loops=1) + -> Index Scan using pkey_only_table_pkey on pkey_only_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- CREATE a table with a multi-column secondary index +DROP TABLE IF EXISTS secindex_only_table; +NOTICE: table "secindex_only_table" does not exist, skipping +CREATE TABLE secindex_only_table (h INT, v1 INT, v2 INT, v3 INT); +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1_v2 ON secindex_only_table((v1, v2) HASH); +INSERT INTO secindex_only_table (SELECT i, i, i, i FROM generate_series(1, 10240) AS i); +-- Setting the secondary index columns in the should trigger a read, but no update of the index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = 1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = h WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 + v2 - h WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Same cases as above, but not providing the primary key +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = 1 WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = h WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 + v2 - h WHERE v1 = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (v1 = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- Queries with non-leading secondary index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET h = h WHERE h = 1; + QUERY PLAN +--------------------------------------------------------------- + Update on secindex_only_table (actual rows=0 loops=1) + -> Seq Scan on secindex_only_table (actual rows=1 loops=1) + Storage Filter: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 10240 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 10240 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +-- +-- CREATE a table having secondary indices with NULL values +DROP TABLE IF EXISTS nullable_table; +NOTICE: table "nullable_table" does not exist, skipping +CREATE TABLE nullable_table (h INT PRIMARY KEY, v1 INT, v2 INT, v3 INT); +CREATE INDEX NONCONCURRENTLY nullable_table_v1_v2 ON nullable_table ((v1, v2) HASH); +INSERT INTO nullable_table (SELECT i, NULL, NULL, NULL FROM generate_series(1, 100) AS i); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET v1 = NULL WHERE h = 1; + QUERY PLAN +-------------------------------------------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Index Scan using nullable_table_pkey on nullable_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET v1 = NULL, v2 = NULL WHERE h = 1; + QUERY PLAN +-------------------------------------------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Index Scan using nullable_table_pkey on nullable_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = 1, v1 = NULL, v2 = NULL WHERE h = 1; + QUERY PLAN +-------------------------------------------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Index Scan using nullable_table_pkey on nullable_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE h = 1; + QUERY PLAN +-------------------------------------------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Index Scan using nullable_table_pkey on nullable_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL; + QUERY PLAN +-------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Result (actual rows=0 loops=1) + One-Time Filter: false + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 0 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL OR v2 = NULL; + QUERY PLAN +-------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Result (actual rows=0 loops=1) + One-Time Filter: false + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 0 + Storage Flush Requests: 0 +(7 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL AND v2 = NULL; + QUERY PLAN +-------------------------------------------------- + Update on nullable_table (actual rows=0 loops=1) + -> Result (actual rows=0 loops=1) + One-Time Filter: false + Storage Read Requests: 0 + Storage Rows Scanned: 0 + Storage Write Requests: 0 + Storage Flush Requests: 0 +(7 rows) + +-- CREATE a table having secondary indices in a colocated database. +-- CREATE a table having indexes of multiple data types +DROP TABLE IF EXISTS number_type_table; +NOTICE: table "number_type_table" does not exist, skipping +CREATE TABLE number_type_table (h INT PRIMARY KEY, v1 INT2, v2 INT4, v3 INT8, v4 BIGINT, v5 FLOAT4, v6 FLOAT8, v7 SERIAL, v8 BIGSERIAL, v9 NUMERIC(10, 4), v10 NUMERIC(100, 40)); +CREATE INDEX NONCONCURRENTLY number_type_table_v1_v2 ON number_type_table (v1 ASC, v2 DESC); +CREATE INDEX NONCONCURRENTLY number_type_table_v3 ON number_type_table (v3 HASH); +CREATE INDEX NONCONCURRENTLY number_type_table_v4_v5_v6 ON number_type_table ((v4, v5) HASH, v6 ASC); +CREATE INDEX NONCONCURRENTLY number_type_table_v7_v8 ON number_type_table (v7 HASH, v8 DESC); +CREATE INDEX NONCONCURRENTLY number_type_table_v9_v10 ON number_type_table (v9 HASH) INCLUDE (v10, v1); +INSERT INTO number_type_table(h, v1, v2, v3, v4, v5, v6, v9, v10) VALUES (0, 1, 2, 3, 4, 5.0, 6.000001, '-9.1234', '-10.123455789'); +INSERT INTO number_type_table(h, v1, v2, v3, v4, v5, v6, v9, v10) VALUES (10, 11, 12, 13, 14, 15.01, 16.000002, '-19.1234', '-20.123455789'); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = 0, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789' WHERE h = 0; + QUERY PLAN +-------------------------------------------------------------------------------------------- + Update on number_type_table (actual rows=0 loops=1) + -> Index Scan using number_type_table_pkey on number_type_table (actual rows=1 loops=1) + Index Cond: (h = 0) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(10 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = h + 1, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789' WHERE h = 0; + QUERY PLAN +-------------------------------------------------------------------------------------------- + Update on number_type_table (actual rows=0 loops=1) + -> Index Scan using number_type_table_pkey on number_type_table (actual rows=1 loops=1) + Index Cond: (h = 0) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Index Write Requests: 10 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 12 + Storage Flush Requests: 1 +(11 rows) + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = h + 1, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789'; + QUERY PLAN +------------------------------------------------------------- + Update on number_type_table (actual rows=0 loops=1) + -> Seq Scan on number_type_table (actual rows=2 loops=1) + Storage Table Read Requests: 3 + Storage Table Rows Scanned: 2 + Storage Table Write Requests: 4 + Storage Index Write Requests: 20 + Storage Flush Requests: 1 + Storage Read Requests: 3 + Storage Rows Scanned: 2 + Storage Write Requests: 24 + Storage Flush Requests: 2 +(11 rows) + +CREATE TYPE mood_type AS ENUM ('sad', 'ok', 'happy'); +DROP TABLE IF EXISTS non_number_type_table; +NOTICE: table "non_number_type_table" does not exist, skipping +CREATE TABLE non_number_type_table(tscol TIMESTAMP PRIMARY KEY, varcharcol VARCHAR(8), charcol CHAR(8), textcol TEXT, linecol LINE, ipcol CIDR, uuidcol UUID, enumcol mood_type); +CREATE INDEX NONCONCURRENTLY non_number_type_table_mixed ON non_number_type_table(tscol, varcharcol); +CREATE INDEX NONCONCURRENTLY non_number_type_table_text ON non_number_type_table((varcharcol, charcol) HASH, textcol ASC); +CREATE INDEX NONCONCURRENTLY non_number_type_table_text_hash ON non_number_type_table(textcol HASH); +-- This should fail as indexes on geometric and network types are not yet supported +CREATE INDEX NONCONCURRENTLY non_number_type_table_text_geom_ip ON non_number_type_table(linecol ASC, ipcol DESC); +ERROR: data type line has no default operator class for access method "lsm" +HINT: You must specify an operator class for the index or define a default operator class for the data type. +CREATE INDEX NONCONCURRENTLY non_number_type_table_uuid_enum ON non_number_type_table(uuidcol ASC, enumcol DESC); +INSERT INTO non_number_type_table VALUES('1999-01-08 04:05:06 -8:00', 'varchar1', 'charpad', 'I am batman', '{1, 2, 3}'::line, '1.2.3.0/24'::cidr, '********-****-4***-****-************'::uuid, 'happy'::mood_type); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE non_number_type_table SET tscol = 'January 8 04:05:06 1999 PST', varcharcol = 'varchar1', charcol = 'charpad', textcol = 'I am not batman :(', linecol = '{1, 2, 3}'::line, ipcol = '1.2.3'::cidr, uuidcol = '********-****-4***-****-************'::uuid, enumcol = 'happy' WHERE tscol = 'January 8 07:05:06 1999 EST'; + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Update on non_number_type_table (actual rows=0 loops=1) + -> Index Scan using non_number_type_table_pkey on non_number_type_table (actual rows=0 loops=1) + Index Cond: (tscol = 'Fri Jan 08 07:05:06 1999'::timestamp without time zone) + Storage Table Read Requests: 1 + Storage Read Requests: 1 + Storage Rows Scanned: 0 + Storage Write Requests: 0 + Storage Flush Requests: 0 +(8 rows) diff --git a/src/postgres/src/test/regress/expected/yb_update_optimize_triggers.out b/src/postgres/src/test/regress/expected/yb_update_optimize_triggers.out new file mode 100644 index 000000000000..e0cdcd3107b3 --- /dev/null +++ b/src/postgres/src/test/regress/expected/yb_update_optimize_triggers.out @@ -0,0 +1,210 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. +CREATE OR REPLACE FUNCTION musical_chair() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + -- Increment a specifc column based on the modulo of the row supplied + RAISE NOTICE 'Musical chairs invoked with h = %', NEW.h; + IF OLD.h % 4 = 0 THEN + NEW.h = NEW.h + 40; + ELSIF OLD.h % 4 = 1 THEN + NEW.v1 = NEW.v1 + 1; + ELSIF OLD.h % 4 = 2 THEN + NEW.v2 = NEW.v2 + 1; + ELSE + NEW.v3 = NEW.v3 + 1; + END IF; + RETURN NEW; +END; +$$; +-- CREATE a table that contains the columns specified in the above function. +DROP TABLE IF EXISTS mchairs_table; +NOTICE: table "mchairs_table" does not exist, skipping +CREATE TABLE mchairs_table (h INT PRIMARY KEY, v1 INT, v2 INT, v3 INT); +INSERT INTO mchairs_table (SELECT i, i, i, i FROM generate_series(1, 12) AS i); +-- Create some indexes to test the behavior of the updates +CREATE INDEX NONCONCURRENTLY mchairs_v1_v2 ON mchairs_table (v1 ASC, v2 DESC); +CREATE INDEX NONCONCURRENTLY mchairs_v3 ON mchairs_table (v3 HASH); +-- Add the trigger that plays musical chairs with the above table +CREATE TRIGGER mchairs_table_trigger BEFORE UPDATE ON mchairs_table FOR EACH ROW EXECUTE FUNCTION musical_chair(); +-- The value of v1 should be incremented twice, index v1_v2 should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 + 1 WHERE h = 1; +NOTICE: Musical chairs invoked with h = 1 + QUERY PLAN +------------------------------------------------------------------------------------ + Update on mchairs_table (actual rows=0 loops=1) + -> Index Scan using mchairs_table_pkey on mchairs_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger mchairs_table_trigger: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(12 rows) + +-- The value of v1 should be updated again, indexes v1_v2, v3 should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h, v3 = v3 + 1 WHERE h = 1; +NOTICE: Musical chairs invoked with h = 1 + QUERY PLAN +------------------------------------------------------------------------------------ + Update on mchairs_table (actual rows=0 loops=1) + -> Index Scan using mchairs_table_pkey on mchairs_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 4 + Trigger mchairs_table_trigger: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 5 + Storage Flush Requests: 1 +(12 rows) + +-- Multi-row scenario affecting 4 successive rows exactly with 4 flushes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h, v1 = v1, v2 = v3, v3 = v2 WHERE h > 8 AND h <= 12; +NOTICE: Musical chairs invoked with h = 11 +NOTICE: Musical chairs invoked with h = 12 +NOTICE: Musical chairs invoked with h = 9 +NOTICE: Musical chairs invoked with h = 10 + QUERY PLAN +--------------------------------------------------------- + Update on mchairs_table (actual rows=0 loops=1) + -> Seq Scan on mchairs_table (actual rows=4 loops=1) + Storage Filter: ((h > 8) AND (h <= 12)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 12 + Storage Table Write Requests: 5 + Storage Index Write Requests: 10 + Storage Flush Requests: 3 + Trigger mchairs_table_trigger: calls=4 + Storage Read Requests: 1 + Storage Rows Scanned: 12 + Storage Write Requests: 15 + Storage Flush Requests: 4 +(13 rows) + +-- Validate the updates +SELECT * FROM mchairs_table ORDER BY h; + h | v1 | v2 | v3 +----+----+----+---- + 1 | 4 | 1 | 2 + 2 | 2 | 2 | 2 + 3 | 3 | 3 | 3 + 4 | 4 | 4 | 4 + 5 | 5 | 5 | 5 + 6 | 6 | 6 | 6 + 7 | 7 | 7 | 7 + 8 | 8 | 8 | 8 + 9 | 10 | 9 | 9 + 10 | 10 | 11 | 10 + 11 | 11 | 11 | 12 + 52 | 12 | 12 | 12 +(12 rows) + +-- The decrement of v1 should be offset by the before row trigger. No indexes +-- should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 - 1 WHERE h = 1; +NOTICE: Musical chairs invoked with h = 1 + QUERY PLAN +------------------------------------------------------------------------------------ + Update on mchairs_table (actual rows=0 loops=1) + -> Index Scan using mchairs_table_pkey on mchairs_table (actual rows=1 loops=1) + Index Cond: (h = 1) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Trigger mchairs_table_trigger: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 1 + Storage Flush Requests: 1 +(11 rows) + +-- Same as above but for the primary key +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h - 40, v1 = v1 + 1 WHERE h = 4; +NOTICE: Musical chairs invoked with h = -36 + QUERY PLAN +------------------------------------------------------------------------------------ + Update on mchairs_table (actual rows=0 loops=1) + -> Index Scan using mchairs_table_pkey on mchairs_table (actual rows=1 loops=1) + Index Cond: (h = 4) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 1 + Storage Index Write Requests: 2 + Trigger mchairs_table_trigger: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 3 + Storage Flush Requests: 1 +(12 rows) + +-- A subtle variation of the above to test the scenario that the decrement of +-- h is applied on the value of h that is returned by the before row trigger. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h - 1, v1 = v1 + 1 WHERE h = 4; +NOTICE: Musical chairs invoked with h = 3 + QUERY PLAN +------------------------------------------------------------------------------------ + Update on mchairs_table (actual rows=0 loops=1) + -> Index Scan using mchairs_table_pkey on mchairs_table (actual rows=1 loops=1) + Index Cond: (h = 4) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 1 + Storage Table Write Requests: 2 + Storage Index Write Requests: 4 + Trigger mchairs_table_trigger: calls=1 + Storage Read Requests: 1 + Storage Rows Scanned: 1 + Storage Write Requests: 6 + Storage Flush Requests: 1 +(12 rows) + +-- Multi-row scenario. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 - 1, v2 = v2 - 1, v3 = v3 - 1 WHERE h > 4 AND h <= 8; +NOTICE: Musical chairs invoked with h = 5 +NOTICE: Musical chairs invoked with h = 6 +NOTICE: Musical chairs invoked with h = 7 +NOTICE: Musical chairs invoked with h = 8 + QUERY PLAN +--------------------------------------------------------- + Update on mchairs_table (actual rows=0 loops=1) + -> Seq Scan on mchairs_table (actual rows=4 loops=1) + Storage Filter: ((h > 4) AND (h <= 8)) + Storage Table Read Requests: 1 + Storage Table Rows Scanned: 12 + Storage Table Write Requests: 5 + Storage Index Write Requests: 14 + Storage Flush Requests: 3 + Trigger mchairs_table_trigger: calls=4 + Storage Read Requests: 1 + Storage Rows Scanned: 12 + Storage Write Requests: 19 + Storage Flush Requests: 4 +(13 rows) + +-- Again, validate the updates +SELECT * FROM mchairs_table ORDER BY h; + h | v1 | v2 | v3 +----+----+----+---- + 1 | 4 | 1 | 2 + 2 | 2 | 2 | 2 + 3 | 3 | 3 | 3 + 5 | 5 | 4 | 4 + 6 | 5 | 6 | 5 + 7 | 6 | 6 | 7 + 9 | 10 | 9 | 9 + 10 | 10 | 11 | 10 + 11 | 11 | 11 | 12 + 43 | 6 | 4 | 4 + 48 | 7 | 7 | 7 + 52 | 12 | 12 | 12 +(12 rows) + +DROP TABLE mchairs_table; diff --git a/src/postgres/src/test/regress/sql/yb_update_optimize_base.sql b/src/postgres/src/test/regress/sql/yb_update_optimize_base.sql new file mode 100644 index 000000000000..a4af25c062b3 --- /dev/null +++ b/src/postgres/src/test/regress/sql/yb_update_optimize_base.sql @@ -0,0 +1,315 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. + +-- CREATE functions that can be triggered upon update to modify various columns +CREATE OR REPLACE FUNCTION no_update() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + RAISE NOTICE 'Trigger "no_update" invoked'; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION increment_v1() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.v1 = NEW.v1 + 1; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION increment_v3() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.v3 = NEW.v3 + 1; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION update_all() RETURNS TRIGGER +LANGUAGE PLPGSQL AS $$ +BEGIN + NEW.h = NEW.h + 1024; + NEW.v1 = NEW.v1 + 1024; + NEW.v2 = NEW.v2 + 1; + RETURN NEW; +END; +$$; + +-- CREATE a simple table with only a primary key +DROP TABLE IF EXISTS pkey_only_table; +CREATE TABLE pkey_only_table (h INT PRIMARY KEY, v INT); +INSERT INTO pkey_only_table (SELECT i, i FROM generate_series(1, 1024) AS i); + +-- A simple point update without involving the primary key +-- This query does not go through the distributed transaction path +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v = v + 1 WHERE h = 1; + +-- Point updates that include the primary key in the targetlist +-- These queries do not go through the distributed transaction path either +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = v - 1, v = v + 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h, v = v + 1 WHERE h = 1; -- Needs further optimization +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = 1, v = v + 1 WHERE h = 1; + +-- Queries affecting a range of rows +-- Since the primary key is not specified in its entirety, these use the distributed txns +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h, v = v + 1 WHERE h > 10 AND h < 15; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = v, v = v + 1 WHERE h > 20 AND h < 25; + +-- Query that updates the primary key. This should involve multiple flushes +-- over a distributed transaction. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h + 1024, v = v + 1 WHERE h < 5; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = h + 1024, v = v + 1 WHERE h < 2000; + +DROP TABLE pkey_only_table; + +-- CREATE a table with no primary key, but having a secondary indexes. +DROP TABLE IF EXISTS secindex_only_table; +CREATE TABLE secindex_only_table (v1 INT, v2 INT, v3 INT); +INSERT INTO secindex_only_table (SELECT i, i, i FROM generate_series(1, 1024) AS i); + +-- Add an index on v1 +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1 ON secindex_only_table (v1); + +-- Updates not involving the secondary index should not have multiple flushes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v3 = v2 + 1 WHERE v1 = 1; +-- This specifically tests the case where there is no overlap between the index +-- and columns that are potentially modified. The index should be added to a +-- skip list without further consideration +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; + +-- Point updates that include the column referenced by the index in the targetlist. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v2, v3 = v3 + 1 WHERE v1 = 1; + +-- Queries affecting a range of rows +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v1 < 5; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v2, v3 = v3 + 1 WHERE v1 < 5; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 + 1 WHERE v2 < 5; + +-- Special case where no column is affected. We should still see a flush in order +-- to acquire necessary row lock on the main table. This is similar to SELECT FOR UPDATEs. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 = 5; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 < 5; + +-- Add a second index to the table which is a multi-column index +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1_v2 ON secindex_only_table (v1, v2); + +-- Queries that affect only one of the two indexes +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; + +-- Same as above but queries that actually modify one of the indexes exclusively. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 15; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v2 = 16; + +-- Queries that cover both indexes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v3 = v3 WHERE v1 = 15; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 15; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 15; + +-- Add a simple trigger to the table. +CREATE TRIGGER secindex_only_table_no_update BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION no_update(); + +-- Repeat the above queries to validate that the number of flushes do not change. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 25; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 25; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 25; + +-- Add a trigger that modifies v3 (no indexes on it) +CREATE TRIGGER secindex_only_table_increment_v3 BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION increment_v3(); +-- Read the values corresponding to v1 = 1. +SELECT * FROM secindex_only_table WHERE v1 = 1; + +-- Repeat a subset of the above queries to validate that no extra flushes are required. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 = 35; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 35; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 35; + +-- Read the values corresponding to v1 = 1 again to validate that v3 is incremented twice: +-- once by the query, once by the trigger. +SELECT * FROM secindex_only_table WHERE v1 = 1; + +-- Add a trigger that modifies v1 (has two indexes on it) +CREATE TRIGGER secindex_only_table_increment_v1 BEFORE UPDATE ON secindex_only_table FOR EACH ROW EXECUTE FUNCTION increment_v1(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2, v3 = v3 WHERE v2 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1, v2 = v2 + 1, v3 = v3 WHERE v1 = 45; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v2 = v2 + 1, v3 = v3 WHERE v1 = 55; + +-- Read the values to confirm that there is/are: +-- No row corresponding to v1 = 45 +-- Two rows for v1 = 46 +-- One row for v1 = 47 +-- No rows for v1 = 55 +-- One row for v1 = 46 +-- Two rows for v1 = 47 +SELECT * FROM secindex_only_table WHERE v1 IN (45, 46, 47, 55, 56, 57) ORDER BY v1, v2; + +-- Query that updates a range of values between 61 and 70. +-- TODO: Validate this +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v2 = v2 + 1, v3 = v3 WHERE v1 > 60 AND v1 <= 70; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 + 1, v3 = v3 + 1 WHERE v1 > 70 AND v1 <= 80; + +DROP TABLE secindex_only_table; + +-- CREATE a table with no primary key or secondary indexes. +DROP TABLE IF EXISTS no_index_table; +CREATE TABLE no_index_table (h INT, v INT); +INSERT INTO no_index_table (SELECT i, i FROM generate_series(1, 1024) AS i); + +-- Point update queries. Irrespective of whether the columns in the targetlist +-- are modified, we should see flushes updating the row. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h, v = v WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = h + 1 WHERE h = 10; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET v = v + 1 WHERE v = 10; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE no_index_table SET h = v + 1, v = h + 1 WHERE v = 20; + +-- CREATE a hierarchy of tables of the order a <-- b <-- c where 'a' is the base +-- table, 'b' references 'a' and is a reference to 'c', and so on. +DROP TABLE IF EXISTS a_test; +DROP TABLE IF EXISTS b1_test; +DROP TABLE IF EXISTS b2_test; +DROP TABLE IF EXISTS c1_test; +DROP TABLE IF EXISTS c2_test; + +CREATE TABLE a_test (h INT PRIMARY KEY, v1 INT UNIQUE, v2 INT); +CREATE INDEX NONCONCURRENTLY a_v1 ON a_test (v1); +CREATE TABLE b1_test (h INT PRIMARY KEY REFERENCES a_test (h), v1 INT UNIQUE REFERENCES a_test (v1), v2 INT); +CREATE INDEX NONCONCURRENTLY b1_v1 ON b1_test (v1); +CREATE TABLE b2_test (h INT PRIMARY KEY REFERENCES a_test (v1), v1 INT REFERENCES a_test (h), v2 INT); +CREATE INDEX NONCONCURRENTLY b2_v1 ON b2_test (v1); + +INSERT INTO a_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); +INSERT INTO b1_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); +INSERT INTO b2_test (SELECT i, i, i FROM generate_series(1, 1024) AS i); + +-- Point update queries on table 'a'. Should skip 'referenced-by' constraints on +-- b-level tables. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h, v1 = h, v2 = v2 + 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET v1 = v1, v2 = v2 + 1 WHERE v1 = h AND h = 2; + +-- Same as above but update the rows to validate 'referenced-by' constraints. +INSERT INTO a_test (SELECT i, i, i FROM generate_series(1025, 1034) AS i); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h, v1 = v1 + 1 WHERE h = 1034; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE a_test SET h = h + 1, v1 = v1 + 1 WHERE h = 1034; + +-- +-- The following sections tests updates when the update optimization is turned +-- off. This is to prevent regressions. +-- +SET yb_update_num_cols_to_compare TO 0; +DROP TABLE IF EXISTS base_table1; +CREATE TABLE base_table1 (k INT PRIMARY KEY, v1 INT, v2 INT); +INSERT INTO base_table1 (SELECT i, i, i FROM generate_series(1, 10) AS i); + +-- This query below should follow the transaction fast path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + +-- The queries below should use the distributed transaction path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = v1 - 1, v1 = v1 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = k, v1 = v1 + 1 WHERE k > 5 AND k < 15; + +-- Adding a trigger should automatically make the table use the distributed +-- transaction path. +CREATE TRIGGER base_table1_no_update BEFORE UPDATE ON base_table1 FOR EACH ROW EXECUTE FUNCTION no_update(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; +DROP TRIGGER base_table1_no_update ON base_table1; + +-- Add a trigger that updates values of v1. +CREATE TRIGGER base_table1_increment_v1 BEFORE UPDATE ON base_table1 FOR EACH ROW EXECUTE FUNCTION increment_v1(); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v2 = v2 + 1 WHERE k = 1; +-- v1 should be updated twice +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; +DROP TRIGGER base_table1_increment_v1 ON base_table1; + +-- Drop the trigger and adding a secondary index should produce the same result. +CREATE INDEX NONCONCURRENTLY base_table1_v1 ON base_table1 (v1); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 WHERE k = 1; + +-- Validate that the values have indeed been updated. +SELECT * FROM base_table1 WHERE k = 1; +-- Use an index-only scan to validate that the index has been updated. +SELECT v1 FROM base_table1 WHERE k = 1; + +DROP TABLE base_table1; + +-- Turn the GUC back on for the remaining tests +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +-- +-- The following section contains a set of sanity tests for colocated tables/databases. +-- +CREATE DATABASE codb colocation = true; +\c codb +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +DROP TABLE IF EXISTS base_table1; +CREATE TABLE base_table1 (k INT PRIMARY KEY, v1 INT, v2 INT) WITH (colocation = true); +INSERT INTO base_table1 (SELECT i, i, i FROM generate_series(1, 10) AS i); + +-- This query below should follow the transaction fast path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET v1 = v1 + 1 WHERE k = 1; + +-- The queries below should use the distributed transaction path. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = v1 - 1, v1 = v1 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = 1, v2 = v2 + 1 WHERE k = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE base_table1 SET k = k, v1 = v1 + 1 WHERE k > 5 AND k < 15; + +DROP TABLE base_table1; + +\c yugabyte +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +-- The queries below test self-referential foreign keys +DROP TABLE IF EXISTS ancestry; +CREATE TABLE ancestry (key TEXT PRIMARY KEY, value TEXT, parent TEXT, gparent TEXT); +ALTER TABLE ancestry ADD CONSTRAINT fk_self_parent_to_key FOREIGN KEY (parent) REFERENCES ancestry (key); +ALTER TABLE ancestry ADD CONSTRAINT fk_self_gparent_to_key FOREIGN KEY (gparent) REFERENCES ancestry (key); + +-- Insert the family tree +INSERT INTO ancestry VALUES ('root', 'Dave', NULL, NULL); +INSERT INTO ancestry VALUES ('ggparent1', 'Adam', 'root', NULL); +INSERT INTO ancestry VALUES ('ggparent2', 'Barry', 'root', NULL); +INSERT INTO ancestry VALUES ('gparent1', 'Claudia', 'ggparent1', 'root'); +INSERT INTO ancestry VALUES ('gparent2', 'Donatello', 'ggparent1', 'root'); +INSERT INTO ancestry VALUES ('gparent3', 'Eddie', 'ggparent2', 'root'); +INSERT INTO ancestry VALUES ('parent1', 'Farooq', 'gparent1', 'ggparent1'); +INSERT INTO ancestry VALUES ('parent2', 'Govind', 'gparent1', 'ggparent2'); +INSERT INTO ancestry VALUES ('child1', 'Harry', 'parent1', 'gparent1'); +INSERT INTO ancestry VALUES ('child2', 'Ivan', 'parent2', 'gparent1'); + +-- Query modifying the value column should not trigger referential constraint check. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET value = 'David' WHERE value = 'Dave'; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'root', value = 'David' WHERE key = 'root'; + +-- Query modifying one's parent should trigger trigger the referential +-- constraint in only one direction to see if the parent exists. Grandparent +-- checks in both directions should be skipped. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET parent = 'ggparent2', gparent = 'root' WHERE value = 'Donatello'; +-- This query should throw an error as the parent doesn't exist +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET parent = 'ggparent23', gparent = 'root' WHERE key = 'gparent2'; +-- Query modifying the key to the row should trigger checks in only the children +-- and grandchildren. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'gparent30' WHERE value = 'Eddie'; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE ancestry SET key = 'gparent3' WHERE key = 'gparent30'; diff --git a/src/postgres/src/test/regress/sql/yb_update_optimize_indices.sql b/src/postgres/src/test/regress/sql/yb_update_optimize_indices.sql new file mode 100644 index 000000000000..c4c8766638a9 --- /dev/null +++ b/src/postgres/src/test/regress/sql/yb_update_optimize_indices.sql @@ -0,0 +1,98 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. + +-- CREATE a table with a primary key and no secondary indexes +DROP TABLE IF EXISTS pkey_only_table; +CREATE TABLE pkey_only_table (h INT PRIMARY KEY, v1 INT, v2 INT); +INSERT INTO pkey_only_table (SELECT i, i, i FROM generate_series(1, 10240) AS i); +EXPLAIN (ANALYZE, DIST, COSTS OFF) SELECT * FROM pkey_only_table; + +-- Updating non-index column should be done in a single op +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, v2 = 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 2 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 3, v2 = 3 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = v1 + 1, v2 = v1 + 1 WHERE h = 1; + +-- Setting index column in the update should trigger a read, but no update of the index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET h = 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = 1 WHERE h = 1; +-- TODO: Fix bug here +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = h WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE pkey_only_table SET v1 = 1, h = v1 WHERE h = 1; + +-- CREATE a table with a multi-column secondary index +DROP TABLE IF EXISTS secindex_only_table; +CREATE TABLE secindex_only_table (h INT, v1 INT, v2 INT, v3 INT); +CREATE INDEX NONCONCURRENTLY secindex_only_table_v1_v2 ON secindex_only_table((v1, v2) HASH); +INSERT INTO secindex_only_table (SELECT i, i, i, i FROM generate_series(1, 10240) AS i); + +-- Setting the secondary index columns in the should trigger a read, but no update of the index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = 1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = h WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 + v2 - h WHERE h = 1; + +-- Same cases as above, but not providing the primary key +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = 1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = v1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = h WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 WHERE v1 = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET v1 = h, h = v1 + v2 - h WHERE v1 = 1; + +-- Queries with non-leading secondary index +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE secindex_only_table SET h = h WHERE h = 1; +-- + +-- CREATE a table having secondary indices with NULL values +DROP TABLE IF EXISTS nullable_table; +CREATE TABLE nullable_table (h INT PRIMARY KEY, v1 INT, v2 INT, v3 INT); +CREATE INDEX NONCONCURRENTLY nullable_table_v1_v2 ON nullable_table ((v1, v2) HASH); +INSERT INTO nullable_table (SELECT i, NULL, NULL, NULL FROM generate_series(1, 100) AS i); + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET v1 = NULL WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET v1 = NULL, v2 = NULL WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = 1, v1 = NULL, v2 = NULL WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE h = 1; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL OR v2 = NULL; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE nullable_table SET h = h, v1 = NULL, v2 = NULL WHERE v1 = NULL AND v2 = NULL; + +-- CREATE a table having secondary indices in a colocated database. + + +-- CREATE a table having indexes of multiple data types +DROP TABLE IF EXISTS number_type_table; +CREATE TABLE number_type_table (h INT PRIMARY KEY, v1 INT2, v2 INT4, v3 INT8, v4 BIGINT, v5 FLOAT4, v6 FLOAT8, v7 SERIAL, v8 BIGSERIAL, v9 NUMERIC(10, 4), v10 NUMERIC(100, 40)); +CREATE INDEX NONCONCURRENTLY number_type_table_v1_v2 ON number_type_table (v1 ASC, v2 DESC); +CREATE INDEX NONCONCURRENTLY number_type_table_v3 ON number_type_table (v3 HASH); +CREATE INDEX NONCONCURRENTLY number_type_table_v4_v5_v6 ON number_type_table ((v4, v5) HASH, v6 ASC); +CREATE INDEX NONCONCURRENTLY number_type_table_v7_v8 ON number_type_table (v7 HASH, v8 DESC); +CREATE INDEX NONCONCURRENTLY number_type_table_v9_v10 ON number_type_table (v9 HASH) INCLUDE (v10, v1); + +INSERT INTO number_type_table(h, v1, v2, v3, v4, v5, v6, v9, v10) VALUES (0, 1, 2, 3, 4, 5.0, 6.000001, '-9.1234', '-10.123455789'); +INSERT INTO number_type_table(h, v1, v2, v3, v4, v5, v6, v9, v10) VALUES (10, 11, 12, 13, 14, 15.01, 16.000002, '-19.1234', '-20.123455789'); + +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = 0, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789' WHERE h = 0; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = h + 1, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789' WHERE h = 0; +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE number_type_table SET h = h + 1, v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6.000001, v7 = 1, v8 = 1, v9 = '-9.12344', v10 = '-10.123455789'; + +CREATE TYPE mood_type AS ENUM ('sad', 'ok', 'happy'); +DROP TABLE IF EXISTS non_number_type_table; +CREATE TABLE non_number_type_table(tscol TIMESTAMP PRIMARY KEY, varcharcol VARCHAR(8), charcol CHAR(8), textcol TEXT, linecol LINE, ipcol CIDR, uuidcol UUID, enumcol mood_type); +CREATE INDEX NONCONCURRENTLY non_number_type_table_mixed ON non_number_type_table(tscol, varcharcol); +CREATE INDEX NONCONCURRENTLY non_number_type_table_text ON non_number_type_table((varcharcol, charcol) HASH, textcol ASC); +CREATE INDEX NONCONCURRENTLY non_number_type_table_text_hash ON non_number_type_table(textcol HASH); +-- This should fail as indexes on geometric and network types are not yet supported +CREATE INDEX NONCONCURRENTLY non_number_type_table_text_geom_ip ON non_number_type_table(linecol ASC, ipcol DESC); +CREATE INDEX NONCONCURRENTLY non_number_type_table_uuid_enum ON non_number_type_table(uuidcol ASC, enumcol DESC); + +INSERT INTO non_number_type_table VALUES('1999-01-08 04:05:06 -8:00', 'varchar1', 'charpad', 'I am batman', '{1, 2, 3}'::line, '1.2.3.0/24'::cidr, 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid, 'happy'::mood_type); +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE non_number_type_table SET tscol = 'January 8 04:05:06 1999 PST', varcharcol = 'varchar1', charcol = 'charpad', textcol = 'I am not batman :(', linecol = '{1, 2, 3}'::line, ipcol = '1.2.3'::cidr, uuidcol = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid, enumcol = 'happy' WHERE tscol = 'January 8 07:05:06 1999 EST'; diff --git a/src/postgres/src/test/regress/sql/yb_update_optimize_triggers.sql b/src/postgres/src/test/regress/sql/yb_update_optimize_triggers.sql new file mode 100644 index 000000000000..566dc6e9177c --- /dev/null +++ b/src/postgres/src/test/regress/sql/yb_update_optimize_triggers.sql @@ -0,0 +1,62 @@ +SET yb_fetch_row_limit TO 1024; +SET yb_explain_hide_non_deterministic_fields TO true; +SET yb_update_num_cols_to_compare TO 50; +SET yb_update_max_cols_size_to_compare TO 10240; + +-- This test requires the t-server gflag 'ysql_skip_row_lock_for_update' to be set to false. + +CREATE OR REPLACE FUNCTION musical_chair() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + -- Increment a specifc column based on the modulo of the row supplied + RAISE NOTICE 'Musical chairs invoked with h = %', NEW.h; + IF OLD.h % 4 = 0 THEN + NEW.h = NEW.h + 40; + ELSIF OLD.h % 4 = 1 THEN + NEW.v1 = NEW.v1 + 1; + ELSIF OLD.h % 4 = 2 THEN + NEW.v2 = NEW.v2 + 1; + ELSE + NEW.v3 = NEW.v3 + 1; + END IF; + RETURN NEW; +END; +$$; + +-- CREATE a table that contains the columns specified in the above function. +DROP TABLE IF EXISTS mchairs_table; +CREATE TABLE mchairs_table (h INT PRIMARY KEY, v1 INT, v2 INT, v3 INT); +INSERT INTO mchairs_table (SELECT i, i, i, i FROM generate_series(1, 12) AS i); + +-- Create some indexes to test the behavior of the updates +CREATE INDEX NONCONCURRENTLY mchairs_v1_v2 ON mchairs_table (v1 ASC, v2 DESC); +CREATE INDEX NONCONCURRENTLY mchairs_v3 ON mchairs_table (v3 HASH); + +-- Add the trigger that plays musical chairs with the above table +CREATE TRIGGER mchairs_table_trigger BEFORE UPDATE ON mchairs_table FOR EACH ROW EXECUTE FUNCTION musical_chair(); + +-- The value of v1 should be incremented twice, index v1_v2 should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 + 1 WHERE h = 1; +-- The value of v1 should be updated again, indexes v1_v2, v3 should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h, v3 = v3 + 1 WHERE h = 1; +-- Multi-row scenario affecting 4 successive rows exactly with 4 flushes. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h, v1 = v1, v2 = v3, v3 = v2 WHERE h > 8 AND h <= 12; + +-- Validate the updates +SELECT * FROM mchairs_table ORDER BY h; + +-- The decrement of v1 should be offset by the before row trigger. No indexes +-- should be updated. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 - 1 WHERE h = 1; +-- Same as above but for the primary key +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h - 40, v1 = v1 + 1 WHERE h = 4; +-- A subtle variation of the above to test the scenario that the decrement of +-- h is applied on the value of h that is returned by the before row trigger. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET h = h - 1, v1 = v1 + 1 WHERE h = 4; + +-- Multi-row scenario. +EXPLAIN (ANALYZE, DIST, COSTS OFF) UPDATE mchairs_table SET v1 = v1 - 1, v2 = v2 - 1, v3 = v3 - 1 WHERE h > 4 AND h <= 8; + +-- Again, validate the updates +SELECT * FROM mchairs_table ORDER BY h; + +DROP TABLE mchairs_table; diff --git a/src/postgres/src/test/regress/yb_update_optimized_schedule b/src/postgres/src/test/regress/yb_update_optimized_schedule new file mode 100644 index 000000000000..ac68e229529d --- /dev/null +++ b/src/postgres/src/test/regress/yb_update_optimized_schedule @@ -0,0 +1,8 @@ +# src/test/regress/yb_update_optimized_schedule +# +#################################################################################################### +# Testsuite for tests covering UPDATE query optimizations. +#################################################################################################### +test: yb_update_optimize_base +test: yb_update_optimize_indices +test: yb_update_optimize_triggers