cockroachdb / cockroachdb/cockroach
sql: consider propagating locks through CTEs
- Dominant language
- Go
- Stars
- 32.5k
- Forks
- 4.1k
- PR merge metrics
- PR metrics pending
Description
For-update locks are push into reads of `UPDATE` statements to reduce contention. For example, notice the for-update locks in the scan and index join below:
```sql
CREATE TABLE abc (
a INT PRIMARY KEY,
b INT,
c INT,
INDEX (b)
);
EXPLAIN (VERBOSE)
UPDATE abc SET c = c + 1 WHERE b = 10 RETURNING a;
-- info
-- ---------------------------------------------------------------------------------------------------------------------
-- distribution: local
-- vectorized: true
--
-- • update
-- │ columns: (a)
-- │ estimated row count: 10 (missing stats)
-- │ table: abc
-- │ set: c
-- │ auto commit
-- │
-- └── • render
-- │ columns: (a, b, c, c_new)
-- │ render c_new: c + 1
-- │ render a: a
-- │ render b: b
-- │ render c: c
-- │
-- └── • index join
-- │ columns: (a, b, c)
-- │ estimated row count: 10 (missing stats)
-- │ table: abc@abc_pkey
-- │ key columns: a
-- │ locking strength: for update
-- │
-- └── • scan
-- columns: (a, b)
-- estimated row count: 10 (missing stats)
-- table: abc@abc_b_idx
-- spans: /10-/11
-- locking strength: for update
```
If we rewrite the query to use a CTE, the scan of `abc_b_idx` and the lookup join no longer have for-update locks:
```sql
EXPLAIN (VERBOSE)
WITH l AS (
SELECT a FROM abc WHERE b = 10
), d AS (
UPDATE abc SET c = c + 1 WHERE a IN (SELECT a FROM l) RETURNING a
)
SELECT * FROM l;
-- info
-- -----------------------------------------------------------------------------------------
-- distribution: local
-- vectorized: true
--
-- • root
-- │ columns: (a)
-- │
-- ├── • scan buffer
-- │ columns: (a)
-- │ estimated row count: 10 (missing stats)
-- │ label: buffer 1 (l)
-- │
-- ├── • subquery
-- │ │ id: @S1
-- │ │ original sql: SELECT a FROM abc WHERE b = 10
-- │ │ exec mode: discard all rows
-- │ │
-- │ └── • buffer
-- │ │ columns: (a)
-- │ │ label: buffer 1 (l)
-- │ │
-- │ └── • project
-- │ │ columns: (a)
-- │ │
-- │ └── • scan
-- │ columns: (a, b)
-- │ estimated row count: 10 (missing stats)
-- │ table: abc@abc_b_idx
-- │ spans: /10-/11
-- │
-- └── • subquery
-- │ id: @S2
-- │ original sql: UPDATE abc SET c = c + 1 WHERE a IN (SELECT a FROM l) RETURNING a
-- │ exec mode: discard all rows
-- │
-- └── • buffer
-- │ columns: (a)
-- │ label: buffer 2 (d)
-- │
-- └── • update
-- │ columns: (a)
-- │ estimated row count: 10 (missing stats)
-- │ table: abc
-- │ set: c
-- │
-- └── • render
-- │ columns: (a, b, c, c_new)
-- │ render c_new: c + 1
-- │ render a: a
-- │ render b: b
-- │ render c: c
-- │
-- └── • project
-- │ columns: (a, b, c)
-- │
-- └── • lookup join (inner)
-- │ columns: (a, a, b, c)
-- │ estimated row count: 10 (missing stats)
-- │ table: abc@abc_pkey
-- │ equality: (a) = (a)
-- │ equality cols are key
-- │
-- └── • scan buffer
-- columns: (a)
-- estimated row count: 10 (missing stats)
-- label: buffer 1 (l)
```
If a CTE is reference by an `UPDATE` statement, it would be nice if we pushed locks across the CTE boundary. Is this safe to do in all cases or only when the CTE is _only_ referenced by mutations?
Jira issue: CRDB-49230
Contributor guide
Assessment
This issue has not been assessed yet.