mirror of
https://github.com/scylladb/scylladb.git
synced 2026-05-30 19:46:48 +00:00
ScyllaDB has special counter columns for which atomic add/subtract
operations like `SET a = a + 1` are allowed. Such operations have not
been allowed on ordinary non-counter columns, as they would not be
properly atomic - the read an the write are separate, and concurrent
operations can have incorrect results.
This patch makes it allowed to use such atomic add/subtract operations
in *LWT* statements. Some examples:
UPDATE ... SET a = a - 1 IF a > 0
UPDATE ... SET a = a + 1 IF EXISTS
UPDATE ... SET a = a + 1 a != NULL
The row updated in the operation, and the updated column (a) should
be initialized before the update - arithmetic operations on missing
column values silently leave the column null (no error is generated).
This add/subtract operations is allowed on any numeric column -
integer or floating point of any size.
The ability of LWT to fetch the old values of a column and use it to
calculate the new value has long been available in our internal CAS
implementation - and has been in use for years in Alternator - but until
this patch it was not exposed in CQL's LWT.
This patch does not add new syntax to CQL - the "SET a = a + b"
and "SET a = a - b" syntax that already existed for counters is now
allowed for non-counters.
This is a new Scylla-only feature that does not exist in Cassandra.
Fixes #10568
Signed-off-by: Nadav Har'El <nyh@scylladb.com>
111 lines
7.2 KiB
ReStructuredText
111 lines
7.2 KiB
ReStructuredText
====================================================
|
||
How does ScyllaDB LWT Differ from Apache Cassandra ?
|
||
====================================================
|
||
|
||
ScyllaDB is making an effort to be compatible with Cassandra, down to the level of limitations of the implementation.
|
||
How is it different?
|
||
|
||
* ScyllaDB most commonly uses fewer rounds than Cassandra to complete a lightweight transaction. While Cassandra issues a separate read query to fetch the old record, scylla piggybacks the read result on the response to the prepare round.
|
||
* ScyllaDB will automatically use synchronous commit log write mode for all lightweight transaction writes. Before a lightweight transaction completes, scylla will ensure that the data in it has hit the device. This is done in all commitlog_sync modes.
|
||
* Conditional statements return a result set, and unlike Cassandra, ScyllaDB result set metadata doesn’t change from execution to execution: ScyllaDB always returns the old version of the row, regardless of whether the condition is true or not. This ensures conditional statements work well with prepared statements.
|
||
* For batch statement, the returned result set contains an old row for every conditional statement in the batch, in statement order. Cassandra returns results in clustering key order.
|
||
* For batch statement, ScyllaDB allows mixing `IF EXISTS`, `IF NOT EXISTS`, and other conditions for the same row.
|
||
* Unlike Cassandra, ScyllaDB uses per-core data partitioning, so the RPC that is done to perform a transaction talks directly to the right core on a peer replica, avoiding the concurrency overhead. This is, of course, true, if ScyllaDB’s own shard-aware driver is used - otherwise we add an extra hop to the right core at the coordinator node.
|
||
* ScyllaDB does not store hints for lightweight transaction writes, since this is redundant as all such writes are already present in system.paxos table.
|
||
* ScyllaDB supports arithmetic ``SET`` operations (``col = col + value`` and ``col = col - value``) on non-counter numeric columns inside LWT conditional updates. Cassandra does not support this syntax.
|
||
|
||
|
||
More on :doc:`Lightweight Transactions (LWT) </features/lwt>`
|
||
|
||
Additional Notes
|
||
================
|
||
|
||
Arithmetic SET operations in LWT updates
|
||
----------------------------------------
|
||
|
||
ScyllaDB allows ``SET col = col + value`` and ``SET col = col - value`` on non-counter numeric
|
||
columns inside a conditional ``UPDATE``. The Paxos read-modify-write cycle guarantees atomicity:
|
||
the old value is read in the same transaction round, the arithmetic is applied, and the result
|
||
is written — all without a separate read or a race condition.
|
||
|
||
If the column is null, the arithmetic result is also null (standard SQL null-propagation
|
||
semantics), so the column remains unset. To catch an uninitialized column, use an ``IF``
|
||
condition that checks the column value (e.g. ``IF score != null``) rather than ``IF EXISTS``.
|
||
|
||
This syntax is a ScyllaDB extension; Cassandra rejects it with an ``InvalidRequest`` error.
|
||
An ``IF`` condition (``IF EXISTS`` or a column predicate) is required; omitting the ``IF``
|
||
clause is rejected even in ScyllaDB, because without LWT atomicity the read-modify-write
|
||
would not be safe.
|
||
|
||
Example — atomic counter on a regular column:
|
||
|
||
.. code-block:: cql
|
||
|
||
-- Initialize a row, with score=0
|
||
INSERT INTO mytable (pk, ck, score) VALUES (1, 1, 0) IF NOT EXISTS;
|
||
|
||
-- Increment atomically:
|
||
UPDATE mytable SET score = score + 1 WHERE pk = 1 AND ck = 1 IF EXISTS;
|
||
|
||
-- Increment only when score is already set (fails if score is null):
|
||
UPDATE mytable SET score = score + 1 WHERE pk = 1 AND ck = 1 IF score != null;
|
||
|
||
-- Decrement only when the value is large enough:
|
||
UPDATE mytable SET score = score - 5 WHERE pk = 1 AND ck = 1 IF score >= 5;
|
||
|
||
|
||
Mixing LWT IF clauses in BATCH statements
|
||
-----------------------------------------
|
||
|
||
`Conditional batches` are `BATCH` statements that contain one or more conditional statements. A batch is executed only if all conditions in all statements are true, and all conditions are evaluated against the initial database state. ScyllaDB allows using different conditions such as `IF EXISTS`, `IF NOT EXISTS`, and other `IF` expressions within the same batch, even if the statements affect the same row.
|
||
|
||
For example, the following batch statement is valid in ScyllaDB and will be applied successfully:
|
||
|
||
.. code-block:: cql
|
||
|
||
BEGIN BATCH
|
||
UPDATE movies.nowshowing SET main_actor = NULL WHERE movie = 'Invisible Man' IF director = 'Leigh Whannell'
|
||
UPDATE movies.nowshowing SET released = NULL WHERE movie = 'Invisible Man' IF EXISTS
|
||
APPLY BATCH;
|
||
|
||
[applied] | movie | location | run_day | run_time | director | main_actor | released | theater
|
||
-----------+---------------+----------+---------+----------+----------------+----------------+------------+---------
|
||
True | Invisible Man | null | null | null | Leigh Whannell | Elisabeth Moss | 2022-04-06 | null
|
||
True | Invisible Man | null | null | null | Leigh Whannell | Elisabeth Moss | 2022-04-06 | null
|
||
|
||
By contrast, Cassandra does not allow mixing `IF EXISTS`, `IF NOT EXISTS`, and other conditions for the same row:
|
||
|
||
.. code-block:: cql
|
||
|
||
BEGIN BATCH
|
||
UPDATE movies.nowshowing SET main_actor = NULL WHERE movie = 'Invisible Man' IF director = 'Leigh Whannell'
|
||
UPDATE movies.nowshowing SET released = NULL WHERE movie = 'Invisible Man' IF EXISTS
|
||
APPLY BATCH;
|
||
|
||
InvalidRequest: Error from server: code=2200 [Invalid query] message="Cannot mix IF conditions and IF EXISTS for the same row"
|
||
|
||
Moreover, ScyllaDB does not return an error even if the conditions are impossible to satisfly, i.e. a batch contains `IF EXISTS` and `IF NOT EXISTS` clauses for the same row. The following query will be executed, though not applied, as the conditions are impossible to satisfy:
|
||
|
||
.. code-block:: cql
|
||
|
||
BEGIN BATCH
|
||
INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES ('Invisible Man', 'Times Square', 'AMC Empire 25', 'Saturday', '23:00:00') IF NOT EXISTS
|
||
UPDATE movies.nowshowing SET theater = NULL WHERE movie = 'Invisible Man' AND location = 'Times Square' AND run_day = 'Saturday' AND run_time = '23:00:00' IF EXISTS
|
||
APPLY BATCH;
|
||
|
||
[applied] | movie | location | run_day | run_time | director | main_actor | released | theater
|
||
-----------+-------+----------+---------+----------+----------+------------+----------+---------
|
||
False | null | null | null | null | null | null | null | null
|
||
False | null | null | null | null | null | null | null | null
|
||
|
||
In comparison, Cassandra returns an error in this case:
|
||
|
||
.. code-block:: cql
|
||
|
||
BEGIN BATCH
|
||
INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES ('Invisible Man', 'Times Square', 'AMC Empire 25', 'Saturday', '23:00:00') IF NOT EXISTS
|
||
UPDATE movies.nowshowing SET theater = NULL WHERE movie = 'Invisible Man' AND location = 'Times Square' AND run_day = 'Saturday' AND run_time = '23:00:00' IF EXISTS
|
||
APPLY BATCH;
|
||
|
||
InvalidRequest: Error from server: code=2200 [Invalid query] message="Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row"
|