Cassandra Driver Prepared Statements
Prepared statements are the recommended method for executing CQL queries in production applications. They provide performance benefits, security protection, and enable token-aware routing.
How Prepared Statements Work
Section titled “How Prepared Statements Work”Prepared statements separate query parsing from execution:
Simple Statement (every execution)
Section titled “Simple Statement (every execution)”Prepared Statement
Section titled “Prepared Statement”PREPARE phase (once):
EXECUTE phase (every request):
Performance Benefits
Section titled “Performance Benefits”Reduced Server-Side Overhead
Section titled “Reduced Server-Side Overhead”| Operation | Simple Statement | Prepared Statement |
|---|---|---|
| Parse query | Every request | Once |
| Validate schema | Every request | Once |
| Create plan | Every request | Once |
| Execute | Every request | Every request |
For high-throughput workloads, the parsing overhead is significant:
Throughput comparison (10,000 queries/sec):
Simple statements: 10,000 × (parse + validate + plan + execute) CPU overhead: significant portion spent on parsing (workload-dependent)
Prepared statements: 1 × (parse + validate + plan) 10,000 × (execute only) CPU overhead: minimal for statement handlingToken-Aware Routing
Section titled “Token-Aware Routing”Prepared statements facilitate token-aware routing by providing partition key metadata to the driver:
Without prepared statements, token-aware routing requires explicitly setting the routing key on the statement. Embedded literal values in query strings cannot be automatically extracted for routing.
Prepared Statement Lifecycle
Section titled “Prepared Statement Lifecycle”Preparation
Section titled “Preparation”// Prepare once (typically at application startup)PreparedStatement prepared = session.prepare( "SELECT * FROM users WHERE user_id = ?");The driver:
- Sends PREPARE request to one node
- Receives prepared statement ID and metadata
- Caches the prepared statement locally
- Automatically re-prepares on other nodes as needed
Execution
Section titled “Execution”// Execute many times with different valuesBoundStatement bound = prepared.bind(userId);ResultSet results = session.execute(bound);The driver:
- Looks up cached prepared statement
- Serializes bound values
- Sends EXECUTE request (not the query string)
- Routes token-aware if partition key bound
Automatic Re-Preparation
Section titled “Automatic Re-Preparation”If a node restarts or does not have the prepared statement, the driver automatically re-prepares:
This is transparent to the application.
Binding Values
Section titled “Binding Values”Positional Binding
Section titled “Positional Binding”PreparedStatement prepared = session.prepare( "INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
// Bind by positionBoundStatement bound = prepared.bind( userId, // position 0 "Alice", // position 1 "alice@example.com" // position 2);Named Binding
Section titled “Named Binding”PreparedStatement prepared = session.prepare( "INSERT INTO users (id, name, email) VALUES (:id, :name, :email)");
// Bind by nameBoundStatement bound = prepared.bind() .setUuid("id", userId) .setString("name", "Alice") .setString("email", "alice@example.com");Named binding is more readable and less error-prone for queries with many parameters.
Null Values
Section titled “Null Values”Explicitly bind null values:
// Correct: explicit nullbound.setString("middle_name", null);
// Incorrect: unbound value// Leaves value unset, may cause errorsCaching Prepared Statements
Section titled “Caching Prepared Statements”Driver-Side Cache
Section titled “Driver-Side Cache”Drivers maintain a cache of prepared statements:
Driver Prepared Statement Cache
| Query String | Prepared ID | Metadata |
|---|---|---|
SELECT * FROM users WHERE id=? | 0x8a3f... | [id, name, email] |
INSERT INTO events (...) VALUES | 0x2b7c... | [partition_id, event_id] |
UPDATE users SET name=? WHERE | 0x9d1e... | [name, id] |
Cache Lookup
Cache lookup: O(1) by query string hash
Application-Level Caching
Section titled “Application-Level Caching”Prepare statements once and reuse:
// GOOD: Prepare once, reusepublic class UserRepository { private final PreparedStatement selectUser; private final PreparedStatement insertUser;
public UserRepository(CqlSession session) { this.selectUser = session.prepare( "SELECT * FROM users WHERE id = ?"); this.insertUser = session.prepare( "INSERT INTO users (id, name) VALUES (?, ?)"); }
public User getUser(UUID id) { return session.execute(selectUser.bind(id))...; }}// BAD: Prepare every requestpublic User getUser(UUID id) { // Prepares the same statement repeatedly! PreparedStatement ps = session.prepare( "SELECT * FROM users WHERE id = ?"); return session.execute(ps.bind(id))...;}The driver caches prepared statements, so re-preparing is not catastrophic, but it adds unnecessary overhead.
Schema Changes and Prepared Statements
Section titled “Schema Changes and Prepared Statements”When schema changes, prepared statements may become invalid:
Handling Schema Changes
Section titled “Handling Schema Changes”| Driver Behavior | Description |
|---|---|
| Automatic re-prepare | Driver detects schema change, re-prepares |
| Metadata refresh | Driver updates column metadata |
| Application notification | Some drivers emit events for schema changes |
Best practice: Prepare statements at startup and handle re-preparation transparently. Avoid caching result metadata assumptions.
Batch Statements
Section titled “Batch Statements”Prepared statements can be used in batches:
PreparedStatement insertEvent = session.prepare( "INSERT INTO events (partition_id, event_id, data) VALUES (?, ?, ?)");
BatchStatement batch = BatchStatement.newInstance(BatchType.UNLOGGED) .add(insertEvent.bind(partitionId, event1Id, data1)) .add(insertEvent.bind(partitionId, event2Id, data2)) .add(insertEvent.bind(partitionId, event3Id, data3));
session.execute(batch);Important: Batches should contain statements for the same partition. Cross-partition batches have significant performance overhead.
Prepared Statement Limits
Section titled “Prepared Statement Limits”Cassandra limits prepared statements per node:
| Version | Parameter | Default | Syntax |
|---|---|---|---|
| 4.0 | prepared_statements_cache_size_mb | auto | Integer (MB) |
| 4.1 | prepared_statements_cache_size | auto | Size literal (10MiB, 256KiB) |
| 5.0 | prepared_statements_cache_size | auto | Size literal (10MiB, 256KiB) |
The auto default calculates as 1/256 of heap or 10MiB, whichever is greater.
When cache is full, statements are evicted using a weighted cache algorithm (Caffeine/W-TinyLFU), which approximates frequency-based eviction rather than strict LRU:
Avoiding Cache Churn
Section titled “Avoiding Cache Churn”| Anti-Pattern | Problem |
|---|---|
| Dynamic query generation | Thousands of unique queries fill cache |
| String concatenation in queries | Each variation is separate statement |
| Unbounded IN clauses | IN (?, ?, ?, ...) with varying count |
// BAD: Dynamic IN clause (each size is different prepared statement)String query = "SELECT * FROM users WHERE id IN (" + String.join(",", Collections.nCopies(ids.size(), "?")) + ")";
// BETTER: Fixed batch size or multiple queries// Or use token-range queries for large setsBest Practices
Section titled “Best Practices”| Practice | Rationale |
|---|---|
| Prepare at startup | Amortize preparation cost, fail fast on errors |
| Reuse prepared statements | Avoid redundant cache lookups |
| Use named parameters | More readable, less error-prone |
| Bind all values explicitly | Avoid unbound value errors |
| Use for all production queries | Performance and token-aware routing |
| Avoid dynamic query generation | Prevents cache churn |
Related Documentation
Section titled “Related Documentation”- Load Balancing Policy — Token-aware routing with prepared statements
- CQL Reference — Query syntax