Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#20505] YSQL: Batch explicit row-level locking
Summary: The `SELECT … FOR UPDATE` command, when applied to multiple keys, currently locks each row serially. This approach results in increased latency due to multiple round-trip communications (RPC requests) to the DocDB storage layer for lock acquisition. This can significantly impact the performance of applications relying on transactional consistency for multi-row operations. The primary goal of this revision is to enhance the performance of multi-key `SELECT … FOR UPDATE` queries by implementing a batched locking mechanism. This approach will aggregate lock requests for multiple rows and execute them in a single RPC call to the DocDB layer, thereby reducing latency and improving overall transaction throughput. This revision plans to apply the optimization to all forms of explicit locking supported by PostgreSQL (`FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE`). In terms of implementation, this means buffering operations for many types of `RowMarkType`. To control the batch size, use the gflag as follows: `SET yb_explicit_row_locking_batch_size = <size>;`, where `<size>` is a positive integer. Note that this flag is set to 1 by default, which disables the feature. As an example, consider the following table with a single primary key column in which we insert 100 rows: ``` CREATE TABLE tbl (k INT PRIMARY KEY); INSERT INTO tbl (SELECT i FROM generate_series(1, 100) AS i); ``` Currently, explicitly acquiring row-level locks for all 100 rows results in `Storage Read Requests: 101`, as we are performing one initial read, and then one read for every row we intend to acquire a lock for: ``` yugabyte=# EXPLAIN (ANALYZE, DIST) SELECT * FROM tbl WHERE k <= 100 FOR UPDATE; QUERY PLAN ----------------------------------------------------------------------------------------------------------- LockRows (cost=0.00..112.50 rows=1000 width=36) (actual time=7.027..156.969 rows=100 loops=1) -> Seq Scan on tbl (cost=0.00..102.50 rows=1000 width=36) (actual time=3.021..3.606 rows=100 loops=1) Remote Filter: (k <= 100) Storage Table Read Requests: 1 Storage Table Read Execution Time: 2.488 ms Storage Table Rows Scanned: 100 Planning Time: 0.098 ms Execution Time: 157.274 ms Storage Read Requests: 101 Storage Read Execution Time: 139.504 ms Storage Rows Scanned: 200 Storage Write Requests: 0 Catalog Read Requests: 0 Catalog Write Requests: 0 Storage Flush Requests: 0 Storage Execution Time: 139.504 ms Peak Memory Usage: 24 kB (17 rows) ``` By reducing the number of RPCs with this optimization, we end up with `Storage Read Requests: 2`. This is because we are performing one initial read request followed by another request for the locks, hence significantly reducting the total execution time: ``` yugabyte=# SET yb_explicit_row_locking_batch_size = 1024; SET yugabyte=# EXPLAIN (ANALYZE, DIST) SELECT * FROM tbl WHERE k <= 100 FOR UPDATE; QUERY PLAN ----------------------------------------------------------------------------------------------------------- LockRows (cost=0.00..112.50 rows=1000 width=36) (actual time=3.883..19.285 rows=100 loops=1) -> Seq Scan on tbl (cost=0.00..102.50 rows=1000 width=36) (actual time=3.810..4.375 rows=100 loops=1) Remote Filter: (k <= 100) Storage Table Read Requests: 1 Storage Table Read Execution Time: 2.532 ms Storage Table Rows Scanned: 100 Planning Time: 0.970 ms Execution Time: 19.621 ms Storage Read Requests: 2 Storage Read Execution Time: 2.535 ms Storage Rows Scanned: 200 Storage Write Requests: 0 Catalog Read Requests: 0 Catalog Write Requests: 0 Storage Flush Requests: 0 Storage Execution Time: 2.535 ms Peak Memory Usage: 24 kB (17 rows) ``` Jira: DB-9512 Test Plan: Added a new SQL regress test `yb_explicit_row_lock_batching.sql/.out` to `yb_misc_serial4_schedule`, which can be run with the following command: `./yb_build.sh --java-test 'org.yb.pgsql.TestPgRegressMisc#testPgRegressMiscSerial4'` The test is based off `yb_explicit_row_lock_planning.sql/.out`, but includes `EXPLAIN (ANALYZE, DIST)` commands with deterministic fields to track the number of requests, ensuring that we are flushing once. Also, there are some newly added cases, such as: - Simple `JOIN` with top-level locking - `JOIN` with leaf-level locking (sub-query) - When `LIMIT` returns less than filtered query - Filter on the Postgres side, with `NOW()` Reviewers: kramanathan, dmitry Reviewed By: kramanathan, dmitry Subscribers: yql, smishra, patnaik.balivada Tags: #jenkins-ready Differential Revision: https://phorge.dev.yugabyte.com/D32543
- Loading branch information