Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

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.


Prepared statements separate query parsing from execution:

ApplicationCassandraApplicationApplicationCassandraCassandraSELECT * FROM users WHERE id = 'abc123'Parse queryValidate schemaCreate execution planExecute queryResults

PREPARE phase (once):

ApplicationCassandraApplicationApplicationCassandraCassandraPREPARE: SELECT * FROM users WHERE id = ?Parse queryValidate schemaCreate execution planCache plan with IDPrepared ID + column metadata

EXECUTE phase (every request):

ApplicationCassandraApplicationApplicationCassandraCassandraEXECUTE: Prepared ID + [bound values]Look up cached planExecute (no parsing)Results

OperationSimple StatementPrepared Statement
Parse queryEvery requestOnce
Validate schemaEvery requestOnce
Create planEvery requestOnce
ExecuteEvery requestEvery 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 handling

Prepared statements facilitate token-aware routing by providing partition key metadata to the driver:

ApplicationDriverNode 1 .Replica.Node 2Node 3ApplicationApplicationDriverDriverNode 1 (Replica)Node 1 (Replica)Node 2Node 2Node 3Node 3Preparation PhasePrepare: SELECT * FROM usersWHERE user_id = ? AND region = ?PREPARE requestPrepared ID + Metadata:- user_id: partition key[0]- region: partition key[1]Execution PhaseExecute with user_id='abc', region='us-east'Extract partition key valuesCalculate token = murmur3('abc', 'us-east')Token maps to Node 1EXECUTE (direct to replica)Skipped - not a replicaSkipped - not a replicaResultsResults

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.


// Prepare once (typically at application startup)
PreparedStatement prepared = session.prepare(
"SELECT * FROM users WHERE user_id = ?");

The driver:

  1. Sends PREPARE request to one node
  2. Receives prepared statement ID and metadata
  3. Caches the prepared statement locally
  4. Automatically re-prepares on other nodes as needed
// Execute many times with different values
BoundStatement bound = prepared.bind(userId);
ResultSet results = session.execute(bound);

The driver:

  1. Looks up cached prepared statement
  2. Serializes bound values
  3. Sends EXECUTE request (not the query string)
  4. Routes token-aware if partition key bound

If a node restarts or does not have the prepared statement, the driver automatically re-prepares:

ApplicationDriverNode 2ApplicationApplicationDriverDriverNode 2Node 2Execute prepared statementEXECUTE (Prepared ID + values)Statement not in cacheUNPREPARED errorPREPARE (query string)Parse and cachePrepared IDEXECUTE (Prepared ID + values)Execute queryResultsResults

This is transparent to the application.


PreparedStatement prepared = session.prepare(
"INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
// Bind by position
BoundStatement bound = prepared.bind(
userId, // position 0
"Alice", // position 1
"alice@example.com" // position 2
);
PreparedStatement prepared = session.prepare(
"INSERT INTO users (id, name, email) VALUES (:id, :name, :email)");
// Bind by name
BoundStatement 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.

Explicitly bind null values:

// Correct: explicit null
bound.setString("middle_name", null);
// Incorrect: unbound value
// Leaves value unset, may cause errors

Drivers maintain a cache of prepared statements:

Driver Prepared Statement Cache

Query StringPrepared IDMetadata
SELECT * FROM users WHERE id=?0x8a3f...[id, name, email]
INSERT INTO events (...) VALUES0x2b7c...[partition_id, event_id]
UPDATE users SET name=? WHERE0x9d1e...[name, id]

Cache Lookup

Cache lookup: O(1) by query string hash

Prepare statements once and reuse:

// GOOD: Prepare once, reuse
public 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 request
public 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.


When schema changes, prepared statements may become invalid:

ApplicationDriverCassandraApplicationApplicationDriverDriverCassandraCassandraInitial StatePrepare SELECT * FROM users WHERE id = ?PREPARE requestPrepared ID + metadata (id, name, email)PreparedStatementSchema Change OccursALTER TABLE users DROP emailNext ExecutionExecute with bound valuesEXECUTE requestDetects schema mismatchSchema changed notificationRe-PREPARE requestNew Prepared ID + metadata (id, name)EXECUTE with new IDResultsResults (without email column)
Driver BehaviorDescription
Automatic re-prepareDriver detects schema change, re-prepares
Metadata refreshDriver updates column metadata
Application notificationSome drivers emit events for schema changes

Best practice: Prepare statements at startup and handle re-preparation transparently. Avoid caching result metadata assumptions.


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.


Cassandra limits prepared statements per node:

VersionParameterDefaultSyntax
4.0prepared_statements_cache_size_mbautoInteger (MB)
4.1prepared_statements_cache_sizeautoSize literal (10MiB, 256KiB)
5.0prepared_statements_cache_sizeautoSize 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:

DriverCassandraDriverDriverCassandraCassandraCache full (10MB)PREPARE new statementEvict statement (weighted cache)Cache new statementPrepared IDLater: Execute evicted statementEXECUTE (evicted statement ID)UNPREPARED errorPREPARE (query string)New Prepared IDEXECUTE (new ID)Results
Anti-PatternProblem
Dynamic query generationThousands of unique queries fill cache
String concatenation in queriesEach variation is separate statement
Unbounded IN clausesIN (?, ?, ?, ...) 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 sets

PracticeRationale
Prepare at startupAmortize preparation cost, fail fast on errors
Reuse prepared statementsAvoid redundant cache lookups
Use named parametersMore readable, less error-prone
Bind all values explicitlyAvoid unbound value errors
Use for all production queriesPerformance and token-aware routing
Avoid dynamic query generationPrevents cache churn