Skip to content

sql: add implicit SELECT FOR SHARE locking to unique checks#107961

Merged
craig[bot] merged 1 commit intocockroachdb:masterfrom
michae2:rc_constraints
Sep 16, 2023
Merged

sql: add implicit SELECT FOR SHARE locking to unique checks#107961
craig[bot] merged 1 commit intocockroachdb:masterfrom
michae2:rc_constraints

Conversation

@michae2
Copy link
Copy Markdown
Collaborator

@michae2 michae2 commented Aug 1, 2023

Unique constraints not directly enforced by a unique index are maintained using unique checks. These unique checks verify the non-existence of conflicting rows during mutation statements. We use unique checks for UNIQUE WITHOUT INDEX constraints and UNIQUE constraints on PARTITION ALL BY and REGIONAL BY ROW tables.

For example, the following insert must use unique checks to verify that there is no conflicting username in any region.

CREATE TABLE users (
  username STRING NOT NULL UNIQUE,
  ...
) LOCALITY REGIONAL BY ROW;

INSERT INTO users VALUES (...);

Under snapshot and read committed isolation, we must lock during all system-maintained constraint checks to prevent concurrent transactions from violating the constraints. This commit adds locking to unique checks, both when those checks verify the constraint and when those checks are used as arbiters in UPSERT and INSERT ON CONFLICT statements.

The locking is:

  • guaranteed-durable, because it is required for correctness
  • for-share locking, because we only need to prevent modification of the checked rows, not prevent reading
  • predicate locking, because we must also prevent insertion of new rows matching the scanned span

This last requirement leads us to create a new locking property, locking form, which distinguishes predicate locking from record locking. This will eventually be passed down to the KV layer but is currently only an optimizer property.

Fixes: #100156

Epic: CRDB-25322

Release note: None

@blathers-crl
Copy link
Copy Markdown

blathers-crl bot commented Aug 1, 2023

It looks like your PR touches production code but doesn't add or edit any test code. Did you consider adding tests to your PR?

🦉 Hoot! I am a Blathers, a bot for CockroachDB. My owner is dev-inf.

@cockroach-teamcity
Copy link
Copy Markdown
Member

This change is Reviewable

@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Aug 3, 2023

@nvanbenschoten is there another term you'd prefer over "range granularity" for these locks? I was thinking about key-range locks used by MS SQL Server under serializable isolation when I called them that, and also this slack conversation, but I can see how that name isn't great. Maybe some other ideas are "span granularity" or something about "gap locking" or "next key locking" as in InnoDB (but maybe that's an implementation detail?).

@michae2 michae2 requested a review from nvb August 3, 2023 21:01
@nvb
Copy link
Copy Markdown
Contributor

nvb commented Aug 3, 2023

The concept of "granularity" is related to but distinct from the concept of locking absent keys (still with single-key granularity). I'm not sure exactly what concept we'd want to express in the optimizer, but I don't think any multi-key locking granularity will be supported by KV any time soon. What KV can support is a lock policy that dictates whether locks should be acquired on "present" keys or "present and absent" keys.

@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Aug 3, 2023

The concept of "granularity" is related to but distinct from the concept of locking absent keys (still with single-key granularity). I'm not sure exactly what concept we'd want to express in the optimizer, but I don't think any multi-key locking granularity will be supported by KV any time soon. What KV can support is a lock policy that dictates whether locks should be acquired on "present" keys or "present and absent" keys.

Makes sense. Would it be alright with you if we gave them the same name in the optimizer regardless of single-key or multi-key, but then did not permit plans using the multi-key version? From the optimizer's perspective the important property is locking both rows and gaps, regardless of the number of keys covered, but I can see that the implementation might be very different in KV for single-key vs multi.

@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Aug 3, 2023

@nvanbenschoten how about something like:

type LockingClairvoyance byte

const (
	LockExisting LockingClairvoyance = iota
	LockPhantoms
)

(I guess Sybase called them phantom locks so maybe something in that vein?)

@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Aug 7, 2023

Latest version uses these names:

type LockingClass byte

const (
       LockRecord LockingClass = iota
       LockPredicate
)

Which better match Postgres' terminology (predicate locks) and the original terminology from The Notions of Consistency and Predicate Locks in a Database System (1976). WDYT?

@nvb
Copy link
Copy Markdown
Contributor

nvb commented Aug 8, 2023

I like "predicate" locks, as it doesn't on its own imply the granularity of such a lock, just that it prevents writes from other transactions that would impact queries with such a predicate.

Maybe LockingForm or LockingStyle instead of LockingClass?

@michae2 michae2 force-pushed the rc_constraints branch 2 times, most recently from 57244ff to c87f245 Compare August 17, 2023 16:52
@michae2 michae2 marked this pull request as ready for review August 17, 2023 16:52
@michae2 michae2 requested a review from a team August 17, 2023 16:52
@michae2 michae2 requested a review from a team as a code owner August 17, 2023 16:52
@michae2 michae2 requested review from DrewKimball and mgartner and removed request for a team August 17, 2023 16:52
@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Aug 17, 2023

This needs more tests but is generally RFAL.

One open question: do we want a session variable like enable_implicit_fk_locking_for_serializable but for uniqueness checks? Workloads that would benefit would have contention around inserting the same unique values. Not sure how common that is, seems generally less useful than the fk one (which came up during an escalation a while back).

Copy link
Copy Markdown
Contributor

@nvb nvb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Reviewed 22 of 22 files at r2, all commit messages.
Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @DrewKimball, @mgartner, and @michae2)


pkg/sql/opt/optbuilder/mutation_builder_unique.go line 418 at r2 (raw file):

				// Unique checks must ensure the non-existence of certain rows
				// (i.e. they error on semi-join instead of anti-join) so we must use
				// predicate locks instead of record locks to prevent insertion of new

"prevent insertion of new rows into the locked span(s) by other concurrent transactions."


pkg/sql/opt/exec/execbuilder/testdata/unique line 4 at r2 (raw file):


statement ok
SET experimental_enable_unique_without_index_constraints = true

What allowed us to remove this? It looks like the default for experimental_enable_unique_without_index_constraints is still false.

Copy link
Copy Markdown
Collaborator Author

@michae2 michae2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more tests and locking during UPSERT and INSERT ON CONFLICT. I think this is RFAL.

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @DrewKimball, @mgartner, and @nvanbenschoten)


pkg/sql/opt/optbuilder/mutation_builder_unique.go line 418 at r2 (raw file):

Previously, nvanbenschoten (Nathan VanBenschoten) wrote…

"prevent insertion of new rows into the locked span(s) by other concurrent transactions."

Done.


pkg/sql/opt/exec/execbuilder/testdata/unique line 4 at r2 (raw file):

Previously, nvanbenschoten (Nathan VanBenschoten) wrote…

What allowed us to remove this? It looks like the default for experimental_enable_unique_without_index_constraints is still false.

Oh, good catch, I deleted this by accident.

Copy link
Copy Markdown
Contributor

@nvb nvb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 39 of 39 files at r3, all commit messages.
Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (waiting on @DrewKimball, @mgartner, and @michae2)


pkg/sql/opt/optbuilder/mutation_builder_arbiter.go line 299 at r3 (raw file):

				// use predicate locks instead of record locks to prevent insertion of
				// new rows into the locked span(s) by other concurrent transactions.
				Form: tree.LockPredicate,

Where will we make the distinction between single-record predicate locks and multi-record predicate locks? Is the thinking to push that all the way down to the logic that selects between Get and Scan? Or to keep it up in optbuilder?


pkg/ccl/logictestccl/testdata/logic_test/regional_by_row_read_committed line 44 at r3 (raw file):

  UNIQUE INDEX (origin),
  INVERTED INDEX (location),
  FAMILY (name, origin, location, region)

This reminds me, do we have an issue describing the changes we'll need to make to support rows with multiple column families under weak isolation?


pkg/sql/logictest/testdata/logic_test/unique_read_committed line 102 at r3 (raw file):

)

statement error pgcode 0A000 guaranteed-durable locking not yet implemented

This will eventually become the "unsupported multi-key predicate lock" error, right?

Copy link
Copy Markdown
Contributor

@mgartner mgartner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm: Nice work!

Reviewed 4 of 22 files at r2, 36 of 39 files at r3, all commit messages.
Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (waiting on @DrewKimball, @michae2, and @nvanbenschoten)


pkg/sql/opt/optbuilder/mutation_builder_arbiter.go line 299 at r3 (raw file):

Previously, nvanbenschoten (Nathan VanBenschoten) wrote…

Where will we make the distinction between single-record predicate locks and multi-record predicate locks? Is the thinking to push that all the way down to the logic that selects between Get and Scan? Or to keep it up in optbuilder?

If tree.LockPredicate is only allowed on single-key spans, this anti-join will have to be converted to a lookup anti-join in order for the locking to work, correct? What happens if the optimized picks a plan without a lookup anti-join?


pkg/sql/opt/optbuilder/mutation_builder_arbiter.go line 403 at r3 (raw file):

				// use predicate locks instead of record locks to prevent insertion of
				// new rows into the locked span(s) by other concurrent transactions.
				Form: tree.LockPredicate,

Similar to my question in the other thread—how do we ensure we'll have single-key spans for this lock form?


pkg/sql/opt/optbuilder/mutation_builder_unique.go line 423 at r3 (raw file):

				// predicate locks instead of record locks to prevent insertion of new
				// rows into the locked span(s) by other concurrent transactions.
				Form: tree.LockPredicate,

And this will require a constrained scan, correct?

Copy link
Copy Markdown
Collaborator Author

@michae2 michae2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, a few more changes:

  • Added join hints so that we're forcing lookup joins.
  • Changed the locking strength of the UPSERT / INSERT ON CONFLICT UPDATE arbiter to be for-update instead of for-share, since we'll be modifying the row if it exists. I'm not sure this is strictly necessary but I think it avoids a lock upgrade.
  • Added some missing validation to validateLockingInFrom.

As mentioned below I might need to fix #108489 for us to always use Gets for these checks, but if so I'd prefer to do that in another PR.

So this should be RFAL.

Reviewable status: :shipit: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @mgartner and @nvanbenschoten)


pkg/sql/opt/optbuilder/mutation_builder_arbiter.go line 299 at r3 (raw file):

Where will we make the distinction between single-record predicate locks and multi-record predicate locks?

I think this will have to be after optimization, because I believe there are some exploration rules that must fire to get single-key spans for RBR tables (e.g. SplitLimitedScanIntoUnionScans and SplitLimitedSelectIntoUnionSelects). It could be as late as kv_batch_fetcher during execution, but that would allow some queries with multi-key predicate locks to succeed (if they never have to create the lock request) which would be confusing. So probably in execbuilder.

If tree.LockPredicate is only allowed on single-key spans, this anti-join will have to be converted to a lookup anti-join in order for the locking to work, correct?

Oh, good point! I've added lookup join hints to all of these joins.

I'm currently trying to determine if #108489 is needed in order to always use Get requests for these checks, or if what we have is sufficient. But if we do need that, I would prefer to do it in another PR.


pkg/sql/opt/optbuilder/mutation_builder_arbiter.go line 403 at r3 (raw file):

Previously, mgartner (Marcus Gartner) wrote…

Similar to my question in the other thread—how do we ensure we'll have single-key spans for this lock form?

Added a lookup join hint, which is the first part.


pkg/sql/opt/optbuilder/mutation_builder_unique.go line 423 at r3 (raw file):

Previously, mgartner (Marcus Gartner) wrote…

And this will require a constrained scan, correct?

Yes, for all of these checks, if we cannot form them as constrained scans (well, specifically, as lookup joins on single-key spans) then we won't be able to execute them under RC.


pkg/sql/logictest/testdata/logic_test/unique_read_committed line 102 at r3 (raw file):

Previously, nvanbenschoten (Nathan VanBenschoten) wrote…

This will eventually become the "unsupported multi-key predicate lock" error, right?

Yes, exactly.

Copy link
Copy Markdown
Contributor

@nvb nvb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm: from my perspective, but @mgartner should also give the recent changes a pass.

Reviewed 37 of 37 files at r4, all commit messages.
Reviewable status: :shipit: complete! 1 of 0 LGTMs obtained (and 1 stale) (waiting on @mgartner and @michae2)


pkg/sql/opt/optbuilder/select.go line 1476 at r4 (raw file):

			// Exclusive locking on the entire row.
		case tree.ForNoKeyUpdate:
			// Exclusive locking on only non-key(s) of the row. Currently unimplemented.

"Currently unimplemented and treated identically to ForUpdate".


pkg/sql/opt/optbuilder/select.go line 1480 at r4 (raw file):

			// Shared locking on the entire row.
		case tree.ForKeyShare:
			// Shared locking on only key(s) of the row. Currently unimplemented.

"Currently unimplemented and treated identically to ForShare".

Copy link
Copy Markdown
Contributor

@mgartner mgartner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

Reviewed 37 of 37 files at r4, all commit messages.
Reviewable status: :shipit: complete! 2 of 0 LGTMs obtained (waiting on @michae2)


pkg/sql/opt/optbuilder/mutation_builder_unique.go line 423 at r3 (raw file):

Previously, michae2 (Michael Erickson) wrote…

Yes, for all of these checks, if we cannot form them as constrained scans (well, specifically, as lookup joins on single-key spans) then we won't be able to execute them under RC.

If there are known cases that we cannot handle we should add some tests and make sure the error messages make sense. I'm worried an error message like "could not produce a plan that conforms to the hints" will be very confusing when the user didn't use explicit hints.

@michae2 michae2 force-pushed the rc_constraints branch 2 times, most recently from a08bec0 to 0457c56 Compare September 15, 2023 23:47
Unique constraints not directly enforced by a unique index are
maintained using unique checks. These unique checks verify the
non-existence of conflicting rows during mutation statements. We use
unique checks for `UNIQUE WITHOUT INDEX` constraints and `UNIQUE`
constraints on `PARTITION ALL BY` and `REGIONAL BY ROW` tables.

For example, the following insert must use unique checks to verify that
there is no conflicting username in any region.

```
CREATE TABLE users (
  username STRING NOT NULL UNIQUE,
  ...
) LOCALITY REGIONAL BY ROW;

INSERT INTO users VALUES (...);
```

Under snapshot and read committed isolation, we must lock during all
system-maintained constraint checks to prevent concurrent transactions
from violating the constraints. This commit adds locking to unique
checks, both when those checks verify the constraint and when those
checks are used as arbiters in `UPSERT` and `INSERT ON CONFLICT`
statements.

The locking is:
- guaranteed-durable, because it is required for correctness
- for-share locking, because we only need to prevent modification of the
  checked rows, not prevent reading
- predicate locking, because we must also prevent insertion of new rows
  matching the scanned span

This last requirement leads us to create a new locking property, locking
form, which distinguishes predicate locking from record locking. This
will eventually be passed down to the KV layer but is currently only an
optimizer property.

Fixes: cockroachdb#100156

Epic: CRDB-25322

Release note: None
@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Sep 16, 2023

Failure is #110645 so I'll go ahead and merge. There will be a follow-up PR. TFTRs!

bors r=nvanbenschoten,mgartner

@craig
Copy link
Copy Markdown
Contributor

craig bot commented Sep 16, 2023

Build succeeded:

@michae2
Copy link
Copy Markdown
Collaborator Author

michae2 commented Sep 27, 2023

Follow-up PR is #110879 which disables unique checks under Read Committed.

craig bot pushed a commit that referenced this pull request Sep 28, 2023
110879: sql: disallow explicit unique checks under read committed isolation r=mgartner,DrewKimball a=michae2

(This is a follow-on PR after #107961.)

Because we do not yet support predicate locking, add a check to execbuilder that disallows any query using it (currently only unique checks under read committed isolation).

Informs: #110873

Release note (sql): We do not yet support explicit unique checks under Read Committed isolation. This means that some `INSERT`, `UPDATE`, and `UPSERT` statements against some `REGIONAL BY ROW` tables will fail under Read Committed isolation with the following error:

```
unimplemented: explicit unique checks are not yet supported under read committed isolation
SQLSTATE: 0A000
```

For more details about which `REGIONAL BY ROW` tables are affected, please see: https://go.crdb.dev/issue-v/110873/v23.2

Co-authored-by: Michael Erickson <michae2@cockroachlabs.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sql: don't allow post-query checks or cascades in weak isolation txns

4 participants