Batch statements allow you to execute multiple CQL statements (prepared or unprepared) in a single request, providing atomicity guarantees depending on the batch type.
Batch Types
ScyllaDB supports three types of batches:
use scylla::statement::batch::BatchType;
// LOGGED batch - atomic and isolated (default)
let logged_batch = Batch::new(BatchType::Logged);
// UNLOGGED batch - not atomic, better performance
let unlogged_batch = Batch::new(BatchType::Unlogged);
// COUNTER batch - for counter updates only
let counter_batch = Batch::new(BatchType::Counter);
Logged Batches
- Atomic: All statements succeed or all fail
- Isolated: Batch appears to execute instantaneously from other operations’ perspective
- Performance: Slower due to batch log overhead
- Use for: Ensuring data consistency across multiple writes
Unlogged Batches
- Not Atomic: Some statements may succeed while others fail
- Performance: Faster, no batch log
- Use for: Grouping independent writes to the same partition for efficiency
Counter Batches
- Specialized: Only for counter updates
- Use for: Batching multiple counter increments/decrements
Creating Batches
Empty Batch
use scylla::statement::batch::{Batch, BatchType};
// Create an empty batch
let mut batch = Batch::new(BatchType::Logged);
// Or use default (Logged)
let mut batch: Batch = Default::default();
Batch with Statements
use scylla::statement::batch::{Batch, BatchType, BatchStatement};
let statements = vec![
BatchStatement::from("INSERT INTO ks.tab(a, b) VALUES(?, ?)"),
BatchStatement::from("INSERT INTO ks.tab(a, b) VALUES(3, ?)"),
];
let batch = Batch::new_with_statements(BatchType::Logged, statements);
Adding Statements
You can add both unprepared and prepared statements to a batch:
use scylla::statement::batch::Batch;
use scylla::statement::unprepared::Statement;
let mut batch: Batch = Default::default();
// Add unprepared statement from &str
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(?, ?)");
// Add unprepared Statement
let statement = Statement::new("INSERT INTO ks.tab(a, b) VALUES(3, ?)");
batch.append_statement(statement);
// Add prepared statement
let prepared = session
.prepare("INSERT INTO ks.tab(a, b) VALUES(5, ?)")
.await?;
batch.append_statement(prepared);
// Add statement with no values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(7, 8)");
For maximum performance, prefer using prepared statements in batches. Unprepared statements with non-empty values will be prepared automatically, requiring additional round trips.
Executing Batches
Batch values must be provided for each statement in the batch:
let mut batch: Batch = Default::default();
// Statement with two bound values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(?, ?)");
// Statement with one bound value
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(3, ?)");
// Statement with no bound values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(5, 6)");
// Batch values: tuple of tuples, one per statement
let batch_values = (
(1_i32, 2_i32), // Two values for first statement
(4_i32,), // One value for second statement
(), // No values for third statement
);
// Execute the batch
session.batch(&batch, batch_values).await?;
Batch Values
The driver accepts any type implementing the BatchValues trait. Common patterns:
Tuple of Tuples
// Each inner tuple corresponds to one statement
let batch_values = (
(1_i32, "text1"), // Statement 1
(2_i32, "text2"), // Statement 2
(3_i32, "text3"), // Statement 3
);
session.batch(&batch, batch_values).await?;
Vector of Tuples
// Useful for dynamic number of statements
let batch_values: Vec<(i32, &str)> = vec![
(1, "text1"),
(2, "text2"),
(3, "text3"),
];
session.batch(&batch, batch_values).await?;
Mixed Statement Types
let mut batch: Batch = Default::default();
// Prepared statement with 2 values
let prepared = session
.prepare("INSERT INTO ks.tab(a, b) VALUES(?, ?)")
.await?;
batch.append_statement(prepared);
// Unprepared statement with 1 value
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(3, ?)");
// Unprepared statement with no values
batch.append_statement("INSERT INTO ks.tab(a, b) VALUES(5, 6)");
let batch_values = (
(1_i32, 2_i32), // For prepared statement
(4_i32,), // For first unprepared statement
(), // For second unprepared statement
);
session.batch(&batch, batch_values).await?;
Configuration
Batches support similar configuration options as individual statements:
Consistency Levels
use scylla::statement::Consistency;
use scylla::statement::SerialConsistency;
let mut batch: Batch = Default::default();
// Set consistency
batch.set_consistency(Consistency::Quorum);
// Set serial consistency for LWT batches
batch.set_serial_consistency(Some(SerialConsistency::LocalSerial));
// Get consistency values
let consistency = batch.get_consistency();
let serial_consistency = batch.get_serial_consistency();
Request Timeout
use std::time::Duration;
let mut batch: Batch = Default::default();
batch.set_request_timeout(Some(Duration::from_secs(30)));
Timestamp
Set a default timestamp for all statements in the batch:
let mut batch: Batch = Default::default();
// Set timestamp in microseconds
let timestamp_micros = 1234567890;
batch.set_timestamp(Some(timestamp_micros));
Idempotence
let mut batch: Batch = Default::default();
// Mark batch as idempotent for safe retries
batch.set_is_idempotent(true);
Tracing
let mut batch: Batch = Default::default();
batch.set_tracing(true);
let result = session.batch(&batch, batch_values).await?;
if let Some(tracing_id) = result.tracing_id() {
println!("Tracing ID: {}", tracing_id);
}
Token-Aware Routing
Batches use the first prepared statement to determine routing:
let mut batch: Batch = Default::default();
// First statement determines routing
let prepared = session
.prepare("INSERT INTO ks.tab(partition_key, value) VALUES(?, ?)")
.await?;
batch.append_statement(prepared);
// Add more statements...
batch.append_statement("INSERT INTO ks.tab(partition_key, value) VALUES(?, ?)");
let batch_values = (
(1_i32, "value1"), // This partition key determines routing
(2_i32, "value2"),
);
session.batch(&batch, batch_values).await?;
If you batch statements by partition key (all statements affect the same partition), you’ll get optimal shard-aware routing. Mixing different partition keys in a batch may result in cross-shard communication.
Best Practices
Use Prepared Statements
Always prefer prepared statements in batches for better performance. Unprepared statements with values require additional preparation round trips.
Batch Same-Partition Writes
For best performance with unlogged batches, group writes to the same partition together to benefit from shard-aware routing.
Limit Batch Size
Keep batch sizes reasonable (typically under 100 statements). Very large batches can cause performance issues and timeouts.
Choose Appropriate Type
Use logged batches only when you need atomicity guarantees. For independent writes, unlogged batches are faster.
Complete Example
use scylla::client::session::{Session, SessionBuilder};
use scylla::statement::batch::{Batch, BatchType};
use scylla::statement::Consistency;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let session: Session = SessionBuilder::new()
.known_node("127.0.0.1:9042")
.build()
.await?;
// Create schema
session
.query_unpaged(
"CREATE KEYSPACE IF NOT EXISTS examples_ks \
WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}",
&[],
)
.await?;
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.tab \
(a int, b int, primary key (a))",
&[],
)
.await?;
// Prepare statement for batch
let prepared = session
.prepare("INSERT INTO examples_ks.tab(a, b) VALUES(?, ?)")
.await?;
// Create and configure batch
let mut batch = Batch::new(BatchType::Logged);
batch.set_consistency(Consistency::Quorum);
// Add statements
batch.append_statement(prepared.clone());
batch.append_statement(prepared.clone());
batch.append_statement(prepared);
// Execute batch with values
let batch_values = (
(1_i32, 10_i32),
(2_i32, 20_i32),
(3_i32, 30_i32),
);
session.batch(&batch, batch_values).await?;
println!("Batch executed successfully");
Ok(())
}
Common Pitfalls
Avoid large batches: Batches with more than a few hundred statements can cause coordinator node overload and timeouts.
Avoid batching single-partition writes: If all writes target the same partition, a single INSERT or UPDATE with a collection is more efficient.
Don’t use batches for reads: Batches are for writes only. Use multiple async queries for parallel reads.
See Also
- Prepared Statements - Optimize batch performance with prepared statements
- Values - How to bind values in batches
- LWT - Using batches with lightweight transactions
- Timeouts - Configuring batch timeouts