Why This Change Matters
A transaction often needs to update more than one table to keep application data consistent. An e-commerce checkout, for example, might reserve inventory, create an order, update payment status, and record an audit entry. These changes may need to succeed or fail together.
In Cloud Spanner, DML lets applications use familiar SQL statements such as INSERT, UPDATE, and DELETE inside read-write transactions. Before this change, developers could encounter the cumulative 80,000-mutation limit when multiple DML statements modified large amounts of data in one transaction.
The new behavior gives developers more flexibility: each DML statement is evaluated against the mutation limit independently. Several statements can collectively generate more than 80,000 mutations while remaining in one transaction, provided each statement stays within its limit and the transaction satisfies the other Spanner constraints.
This is useful when the business operation genuinely requires atomic updates across multiple sets of records.
Understanding Spanner Mutation Limits
A mutation represents a change to database data. The mutation count is not necessarily equal to the number of rows affected. Spanner also accounts for modified cells and relevant index updates.
For example, updating a row with several indexed columns can generate more mutation work than updating a single column without dependent indexes.
The important limits are:
Constraint | What it means |
|---|---|
80,000 mutations per DML statement | Each individual DML statement must remain within the mutation limit, including index changes |
100 MiB transaction size | The transaction must stay within the commit-size limit |
80,000 mutations per Mutation API commit | The separate Mutation API limit remains cumulative across mutations in a commit |
Transaction locking and execution time | Larger transactions can increase contention and the likelihood of retries or aborts |
The change does not remove every limit on transactions. It changes how the mutation limit is enforced for DML statements. The transaction-size limit and other operational constraints still apply.
Before and After the Change
Consider a transaction that performs three DML statements. Each statement generates 35,000 mutations.
Under the previous cumulative approach, the total of 105,000 mutations could exceed the transaction-level limit. Under the updated behavior, each statement is evaluated separately.
DML statement | Mutations generated | Result under the updated rule |
|---|---|---|
Update inventory records | 35,000 | Within the per-statement limit |
Update order records | 35,000 | Within the per-statement limit |
Insert audit records | 35,000 | Within the per-statement limit |
Total | 105,000 | Possible if all other transaction constraints are satisfied |
These figures are illustrative, not benchmark results. Actual mutation counts depend on the schema, modified columns, and indexes.
The distinction is important: a transaction can now exceed 80,000 mutations across multiple DML statements, but a single DML statement cannot simply generate an unlimited number of mutations.
A Practical SQL Example
Suppose an application needs to update product availability, mark old orders for review, and insert audit records as part of a single business operation.
The following GoogleSQL illustrates the individual statements. It assumes that the tables and columns exist and that the application supplies appropriate transaction boundaries.
UPDATE Inventory
SET Status = 'REVIEW'
WHERE WarehouseId = @warehouse_id
AND Quantity < @minimum_quantity;
UPDATE Orders
SET ReviewRequired = TRUE
WHERE WarehouseId = @warehouse_id
AND Status = 'PENDING';
INSERT INTO AuditLog (
AuditId,
EventType,
WarehouseId
)
VALUES (
@audit_id,
'INVENTORY_REVIEW',
@warehouse_id
);These statements represent different parts of one logical operation. When executed inside a single read-write transaction, they can participate in the same atomic commit.
The updated mutation-limit behavior evaluates each DML statement independently. However, the transaction must still satisfy the 100 MiB size limit, and all statements must be valid for the database schema and constraints.
The example also highlights a design consideration: broad UPDATE statements can affect many rows. Before running them in production, inspect the expected row count, verify the filtering conditions, and confirm that the operation belongs in one atomic transaction.
Implementing the Transaction in Java
The change is compatible with existing Spanner client libraries, so applications do not need a special API to use the new limit behavior.
Here is a Java example using the Google Cloud Spanner client library. It runs two DML statements within a read-write transaction.
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.cloud.spanner.TransactionRunner;
public class InventoryService {
private final DatabaseClient databaseClient;
public InventoryService(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}
public void markInventoryForReview(
long warehouseId,
long minimumQuantity) {
databaseClient
.readWriteTransaction()
.run(transaction -> {
Statement updateInventory = Statement.newBuilder(
"UPDATE Inventory " +
"SET Status = @status " +
"WHERE WarehouseId = @warehouseId " +
"AND Quantity < @minimumQuantity")
.bind("status").to("REVIEW")
.bind("warehouseId").to(warehouseId)
.bind("minimumQuantity").to(minimumQuantity)
.build();
transaction.executeUpdate(updateInventory);
Statement updateOrders = Statement.newBuilder(
"UPDATE Orders " +
"SET ReviewRequired = TRUE " +
"WHERE WarehouseId = @warehouseId " +
"AND Status = @orderStatus")
.bind("warehouseId").to(warehouseId)
.bind("orderStatus").to("PENDING")
.build();
transaction.executeUpdate(updateOrders);
return null;
});
}
}
Join the conversation! Your thoughts help the community grow.