You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[#18822] YSQL: Framework to skip redundant sec index updates and fkey checks when relevant columns not modified
Summary:
**Background**
Prior to this revision, an UPDATE statement specifying a list of target columns X in its SET clause, **always** performed the necessary work to update each of the target columns in the storage layer, irrespective of whether the values of the columns actually changed. The necessary work could include requiring locks, updating indexes, checking of constraints, firing of triggers etc.
**The Optimization**
This revision introduces an optimization that validates that the values of a column are indeed being modified, before sending (flushing) the updated value of the column to the storage layer.
In particular, the set of columns whose values that are compared are those that can cause extra round trips to the storage layer in the form of:
- Primary Key Updates
- Secondary Index Updates
- Foreign Key Constraints
- Uniqueness Constraints
The matrix of columns that are marked for update and the objects (indexes, constraints) they impact are computed at planning time.
This is particularly useful when used in conjunction with prepared statements and ORMs, which tend to specify all columns (both modified and non-modified) as part of the target list.
The decision of whether a column is indeed modified is done on a per-tuple basis at execution time.
**Example**
As a concrete example, consider a table with the following schema and data:
```
yugabyte=# CREATE TABLE foo (h INT PRIMARY KEY, v1 INT, v2 INT, v3 INT);
yugabyte=# CREATE INDEX foo_v1_idx ON foo (v1);
yugabyte=# CREATE INDEX foo_v2_idx ON foo (v2);
yugabyte=# INSERT INTO foo (SELECT i, i, i % 10, i % 100 FROM generate_series(1, 10000) AS i);
```
Performing an UPDATE on the first 1000 rows (without the optimization) yields:
```
yugabyte=# SET yb_explain_hide_non_deterministic_fields TO true;
yugabyte=# EXPLAIN (ANALYZE, DIST) UPDATE foo SET h = v1, v1 = v1, v3 = v3 + 1 WHERE v1 <= 1000;
QUERY PLAN
------------------------------------------------------------------------------------------
Update on foo (cost=0.00..105.00 rows=1000 width=88) (actual rows=0 loops=1)
-> Seq Scan on foo (cost=0.00..105.00 rows=1000 width=88) (actual rows=1000 loops=1)
Remote Filter: (v1 <= 1000)
Storage Table Read Requests: 1
Storage Table Rows Scanned: 10000
Storage Table Write Requests: 2000
Storage Index Write Requests: 4000
Storage Flush Requests: 2000
Storage Read Requests: 1
Storage Rows Scanned: 10000
Storage Write Requests: 6000
Storage Flush Requests: 2001
(12 rows)
```
The values of `h` and `v1` are not modified by the query, yet result in multiple write requests to both the main table as well as the secondary indices.
Since updates to key columns (of a table or an index) is executed as a sequence of a DELETE followed by an INSERT, this query requires a large amount of flushes.
This makes the query very expensive in terms of the amount of work to be done.
With the proposed optimization the query is executed as follows:
```
yugabyte=# EXPLAIN (ANALYZE, DIST) UPDATE foo SET h = v1, v1 = v1, v3 = v3 + 1 WHERE v1 <= 1000;
QUERY PLAN
------------------------------------------------------------------------------------------
Update on foo (cost=0.00..105.00 rows=1000 width=88) (actual rows=0 loops=1)
-> Seq Scan on foo (cost=0.00..105.00 rows=1000 width=88) (actual rows=1000 loops=1)
Remote Filter: (v1 <= 1000)
Storage Table Read Requests: 1
Storage Rows Scanned: 10000
Storage Table Write Requests: 1000
Storage Read Requests: 1
Storage Rows Scanned: 10000
Storage Write Requests: 1000
Storage Flush Requests: 1
(10 rows)
```
**Flags and Feature Status**
This revision introduces the following GUCs to control the behavior of this optimization:
`yb_update_num_cols_to_compare` - The maximum number of columns to be compared. (default: 0)
`yb_update_max_cols_size_to_compare` - The maximum size of an individual column that can be compared. (default: 10240)
This feature is currently turned off as a result of setting `yb_update_num_cols_to_compare` to 0.
**Debuggability**
Turn on postgres debug2 logs via the following command:
```
./bin/yb-ctl restart --ysql_pg_conf_csv='log_min_messages=debug2'
```
This produces the following debug information:
```
-- At planning time
2024-07-31 10:59:07.124 PDT [76120] DEBUG: Update matrix: rows represent OID of entities, columns represent attnum of cols
2024-07-31 10:59:07.124 PDT [76120] DEBUG: - 10
2024-07-31 10:59:07.124 PDT [76120] DEBUG: 17415 Y
-- At execution time, on a per-tuple basis
2024-07-31 10:59:07.143 PDT [76120] DEBUG: Index/constraint with oid 17415 requires an update
2024-07-31 10:59:07.143 PDT [76120] DEBUG: Relation: 17412 Columns that are inspected and modified: 1 (10)
2024-07-31 10:59:07.143 PDT [76120] DEBUG: No cols in category: Columns that are inspected and unmodified
2024-07-31 10:59:07.143 PDT [76120] DEBUG: Relation: 17412 Columns that are marked for update: 1 (10) 2 (11)
```
**Future Work**
1. Introduce auto-flag infrastructure to safely use row-locking. This is in the context of upgrade safety while the cluster is being upgraded.
2. As a part of the flag infrastructure, ensure that flags/GUC values are immutable during the lifetime of a query.
3. #22994: PGSQL_UPDATEs with no column references should acquire row locks.
4. #23348: Add support for partitioned tables with out of order columns.
5. Support for serializing optimization metadata in plans.
6. Enhance randgen grammar to support ModifyTable (INSERT/UPDATE/DELETE ) queries
7. #23350: PG 15 support.
jenkins: urgent
Jira: DB-7701
Test Plan:
Run the associated pg_regress test as follows:
```
# New tests
./yb_build.sh --java-test 'org.yb.pgsql.TestPgRegressUpdateOptimized#schedule'
# Existing tests
./yb_build.sh --java-test 'org.yb.pgsql.TestPgUpdatePrimaryKey'
./yb_build.sh --java-test 'org.yb.pgsql.TestPgUniqueConstraint'
./yb_build.sh --java-test 'org.yb.pgsql.TestPgRegressTrigger#testPgRegressTrigger'
./yb_build.sh --java-test 'org.yb.pgsql.TestPgRegressDml#testPgRegressDml'
./yb_build.sh --java-test 'org.yb.pgsql.TestPgRegressPushdown#testPgRegressPushdown'
```
Tested scenarios include (but not limited to):
1. Single row and distributed transactions with and without the feature flag turned on.
2. Relations with a primary key and no secondary indexes or triggers (UPDATEs can take the single row path)
3. Relations with combinations of primary key and secondary indexes.
4. Relations with unconditional before-row triggers.
5. UPDATEs in Colocated databases.
6. UPDATEs covering multiple tuples.
7. Hierarchy of relations with foreign keys
8. Relations with self referential foreign keys
9. Relations with overlapping indexes.
10. Relations having columns with uniqueness constraints.
11. Relations having covering indexes.
12. Relations having partial indexes.
13. Relations having index expressions / predicates.
14. Relations with conditional column triggers.
15. Relations having indexes/constraints out of order (ie. order of columns in relation is different from that of entity)
16. Relations having combination of hash and range indexes.
17. UPDATEs with correlated subqueries.
18. INSERT ON CONFLICT DO UPDATE.
19. UPDATE RETURNING.
20. UPDATEs on temp tables.
Reviewers: mihnea, jason, amartsinchyk
Reviewed By: amartsinchyk
Subscribers: pjain, jason, smishra, yql
Tags: #jenkins-ready
Differential Revision: https://phorge.dev.yugabyte.com/D34040
0 commit comments