-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multi-record UPSERT inserts duplicate values in PRIMARY KEY, resulting in inconsistent results #44466
Comments
I found the cause of this, I'll have to dig in more to see why it happened though:
The upsert seems to delete |
I investigated this more, and it is because the upserter cannot see writes that it makes. It first updates the row from |
The optimizer's intended behavior w.r.t duplicate values in UPSERT are described in #37880. In this case it looks like the PK and the index become inconsistent (the first query returns one row and the second query returns two rows). This makes it more clear:
Regardless of the semantics of UPSERTS this is definitely a bug.. Seems like the "cannot affect row a second time" error should have been triggered. |
My first look suggests that we'd need to add a
I may be missing another option, but I think avoiding problems #1 and #2 above would require KV support. We'd need a way to tell KV to raise an error if a write tries to place an intent on a row that already has an intent from a previous write with seqNum >= X, where X = first seqNum used by the current statement. CC @knz, @andreimatei, @nvanbenschoten, in case they have opinions on:
|
Generally:
That sounds complicated. I'd instead use the new machinery introduced to fix the halloween problem: seqnums that mark statement boundaries. Then the rule is simpler, we can ask MVCC to raise an error when creating an intent if there's already one past the current read seqNum. That's more-or-less a direct translation of pg's rule "no touching the same row twice under the same sub-transaction" The caveat: all these KV-based solutions can only detect duplicate writes when there is a single column family:
What I am trying to convey with these pitfalls is that we need a test suite that exercises all the combinations of code paths and then evaluate the solution using that test suite. If we don't, we'll get weird problems (like the one motivating the discussion). |
I agree with @knz - pushing this complexity into KV is possible, but before doing so, we'd need to make sure that the semantics we want can be defined at the per column family level and not at the per row level. Verifying that seems like a non-trivial task and I don't know that any one person has a good enough sense of when we read and write (and omit reads and writes) to partial rows to be able to say a per column family level check would sufficient. I'd like to hear a bit more about how we could optimize the |
Essentially, it would be the optimizer running DISTINCT ON logic during planning. So it won't be free, but the optimizer already interns |
Heads up we found this blog post and point 6 in there seems to be related. I'd like to invite @karlseguin to post their example here that triggered the dup error on |
@knz, I think you're getting the |
I tried to implement the
|
Re: #2, one janky solution I thought about was creating an expression similar to this for nullable columns in a conflict index:
|
I decided to go ahead with a new variation of the |
Previously, there were cases where an UPSERT or INSERT..ON CONFLICT statement could operate on the same row more than once. This is a problem because these statements are designed to read once to get existing rows, and then to insert or update based on that information. They don't expect that previous writes for the same statement will affect the correctness of subsequent writes. This can lead to index corruption in cases where a row "moves" from one location to another when one of its index keys changes. See issue cockroachdb#44466 for an example. This commit fixes the problem by introducing a new variation of the DistinctOn operator that ensures that the input to the Upsert operator never has duplicates. It differs from the regular DistinctOn operator in two ways: 1. Null behavior: UpsertDistinctOn treats NULL values as not equal to one another for purposes of grouping. Two rows having a NULL- valued grouping column will be placed in different groups. This differs from DistinctOn behavior, where the two rows would be grouped together. This behavior difference reflects SQL semantics, in which a unique index key still allows multiple NULL values. 2. Duplicate behavior: UpsertDistinctOn raises an error if any distinct grouping contains more than one row. It has "input must be distinct" semantics rather than "make the input distinct" semantics. This is used to ensure that no row will be updated more than once. The optbuilder now wraps the input to the Upsert operator with this new UpsertDistinctOn operator. Fixes cockroachdb#44466 Release note (sql change): UPSERT and INSERT..ON CONFLICT statements will sometimes need to do an extra check to ensure that they never update the same row twice. This may adversely affect performance in cases where the optimizer cannot statically prove the extra check is unnecessary.
Previously, there were cases where an UPSERT or INSERT..ON CONFLICT statement could operate on the same row more than once. This is a problem because these statements are designed to read once to get existing rows, and then to insert or update based on that information. They don't expect that previous writes for the same statement will affect the correctness of subsequent writes. This can lead to index corruption in cases where a row "moves" from one location to another when one of its index keys changes. See issue cockroachdb#44466 for an example. This commit fixes the problem by introducing a new variation of the DistinctOn operator that ensures that the input to the Upsert operator never has duplicates. It differs from the regular DistinctOn operator in two ways: 1. Null behavior: UpsertDistinctOn treats NULL values as not equal to one another for purposes of grouping. Two rows having a NULL- valued grouping column will be placed in different groups. This differs from DistinctOn behavior, where the two rows would be grouped together. This behavior difference reflects SQL semantics, in which a unique index key still allows multiple NULL values. 2. Duplicate behavior: UpsertDistinctOn raises an error if any distinct grouping contains more than one row. It has "input must be distinct" semantics rather than "make the input distinct" semantics. This is used to ensure that no row will be updated more than once. The optbuilder now wraps the input to the Upsert operator with this new UpsertDistinctOn operator. Fixes cockroachdb#44466 Release note (sql change): UPSERT and INSERT..ON CONFLICT statements will sometimes need to do an extra check to ensure that they never update the same row twice. This may adversely affect performance in cases where the optimizer cannot statically prove the extra check is unnecessary.
Previously, there were cases where an UPSERT or INSERT..ON CONFLICT statement could operate on the same row more than once. This is a problem because these statements are designed to read once to get existing rows, and then to insert or update based on that information. They don't expect that previous writes for the same statement will affect the correctness of subsequent writes. This can lead to index corruption in cases where a row "moves" from one location to another when one of its index keys changes. See issue cockroachdb#44466 for an example. This commit fixes the problem by introducing a new variation of the DistinctOn operator that ensures that the input to the Upsert operator never has duplicates. It differs from the regular DistinctOn operator in two ways: 1. Null behavior: UpsertDistinctOn treats NULL values as not equal to one another for purposes of grouping. Two rows having a NULL- valued grouping column will be placed in different groups. This differs from DistinctOn behavior, where the two rows would be grouped together. This behavior difference reflects SQL semantics, in which a unique index key still allows multiple NULL values. 2. Duplicate behavior: UpsertDistinctOn raises an error if any distinct grouping contains more than one row. It has "input must be distinct" semantics rather than "make the input distinct" semantics. This is used to ensure that no row will be updated more than once. The optbuilder now wraps the input to the Upsert operator with this new UpsertDistinctOn operator. Fixes cockroachdb#44466 Release note (sql change): UPSERT and INSERT..ON CONFLICT statements will sometimes need to do an extra check to ensure that they never update the same row twice. This may adversely affect performance in cases where the optimizer cannot statically prove the extra check is unnecessary.
45372: sql: ensure that Upsert will never update the same row twice r=andy-kimball a=andy-kimball Previously, there were cases where an UPSERT or INSERT..ON CONFLICT statement could operate on the same row more than once. This is a problem because these statements are designed to read once to get existing rows, and then to insert or update based on that information. They don't expect that previous writes for the same statement will affect the correctness of subsequent writes. This can lead to index corruption in cases where a row "moves" from one location to another when one of its index keys changes. See issue #44466 for an example. This PR fixes the problem by introducing a new variation of the DistinctOn operator that ensures that the input to the Upsert operator never has duplicates. It differs from the regular DistinctOn operator in two ways: 1. Null behavior: UpsertDistinctOn treats NULL values as not equal to one another for purposes of grouping. Two rows having a NULL- valued grouping column will be placed in different groups. This differs from DistinctOn behavior, where the two rows would be grouped together. This behavior difference reflects SQL semantics, in which a unique index key still allows multiple NULL values. 2. Duplicate behavior: UpsertDistinctOn raises an error if any distinct grouping contains more than one row. It has "input must be distinct" semantics rather than "make the input distinct" semantics. This is used to ensure that no row will be updated more than once. The optbuilder now wraps the input to the Upsert operator with this new UpsertDistinctOn operator. In addition, there are several commits that add optimization rules designed to remove these distinct operators when they can be proven to not be necessary. Fixes #44466 Co-authored-by: Andrew Kimball <andyk@cockroachlabs.com>
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
This change works around a CockroachDB implementation quirk wherein an UPSERT is unable to read its own writes, which prevents the same row from being upserted twice within the same statement. This also corrects a potential source of data inconsistency in the applier code, if a single call to Apply contains both upserts and deletes of the same row. Both of the above issues can be addressed by sorting the mutations according to their HLC time and then de-duplicating them by primary key. The newly-added test in the apply package checks for the specific upsert behavior, so that this workaround can be reconsidered if a future version of CockroachDB allows duplicate rows within an UPSERT or CTE. X-Ref: cockroachdb/cockroach#44466 X-Ref: cockroachdb/cockroach#70731 X-Ref: cockroachdb/cockroach#45372
Consider the following testcase:
A single row is fetched, while the following query fetches two records with a duplicate value for c0:
I found this issue in CockroachDB based on commit
1917a12003daad100b491e2b0195d7df5909e63f
.The text was updated successfully, but these errors were encountered: