Skip to main content
Prepared statements are the recommended way to execute queries that will be used multiple times. They offer significant performance and functionality benefits over unprepared statements.

Why Use Prepared Statements?

Performance Benefits

  • Single Parse: Database parses the query only once during preparation
  • Reduced Round Trips: Execute queries in 1 round trip vs 2 for unprepared with values
  • Token-Aware Routing: Driver computes partition keys for optimal node/shard selection
  • Metadata Caching: Can skip result metadata transmission (on supported databases)

Type Safety

Bound values are validated against metadata received from the server during serialization, providing compile-time and runtime type checking.

Load Balancing

The driver can compute partition keys from bound values and route requests directly to the replicas that own the data.

Preparing Statements

Basic Preparation

Use Session::prepare() to create a prepared statement:
use scylla::statement::prepared::PreparedStatement;

// Prepare from a string
let prepared: PreparedStatement = session
    .prepare("INSERT INTO examples_ks.basic (a, b, c) VALUES (?, ?, ?)")
    .await?;

// Prepare from a Statement (with configuration)
use scylla::statement::unprepared::Statement;

let statement = Statement::new("SELECT * FROM ks.table")
    .with_page_size(1000);
    
let prepared = session.prepare(statement).await?;

Cloning Prepared Statements

Cloning a PreparedStatement is cheap - it only copies references to shared data:
let prepared = session.prepare("SELECT * FROM ks.table").await?;

// Clone is cheap - prefer this over re-preparing
let prepared_clone = prepared.clone();
Always prefer cloning over calling prepare() multiple times for the same query. Cloning only copies Arc pointers and is much faster than re-preparing.

Executing Prepared Statements

execute_unpaged - Single Unpaged Request

Executes the statement and returns all results in one response:
let prepared = session
    .prepare("INSERT INTO examples_ks.basic (a, b, c) VALUES (?, 7, ?)")
    .await?;

// Execute with values
session.execute_unpaged(&prepared, (42_i32, "I'm prepared!")).await?;
session.execute_unpaged(&prepared, (43_i32, "I'm prepared 2!")).await?;
session.execute_unpaged(&prepared, (44_i32, "I'm prepared 3!")).await?;

execute_single_page - Manual Paging

Fetches a single page with manual pagination control:
use scylla::response::PagingState;
use std::ops::ControlFlow;

let paged_prepared = session
    .prepare(
        Statement::new("SELECT a, b, c FROM examples_ks.select_paging")
            .with_page_size(7),
    )
    .await?;

let mut paging_state = PagingState::start();
loop {
    let (res, paging_state_response) = session
        .execute_single_page(&paged_prepared, &[], paging_state)
        .await?;

    let res = res.into_rows_result()?;
    println!("Fetched {} rows", res.rows_num());

    match paging_state_response.into_paging_control_flow() {
        ControlFlow::Break(()) => break,
        ControlFlow::Continue(new_paging_state) => {
            paging_state = new_paging_state;
        }
    }
}

execute_iter - Automatic Paging

Returns an async iterator that automatically handles pagination:
use futures::stream::StreamExt;

let prepared = session
    .prepare("SELECT a, b FROM ks.t")
    .await?;

let mut rows_stream = session
    .execute_iter(prepared, &[])
    .await?
    .rows_stream::<(i32, i32)>()?;

while let Some(next_row_res) = rows_stream.next().await {
    let (a, b): (i32, i32) = next_row_res?;
    println!("a, b: {}, {}", a, b);
}

Configuration

Prepared statements inherit configuration from the original Statement and support additional options:

Page Size

let mut prepared = session.prepare("SELECT * FROM ks.table").await?;
prepared.set_page_size(1000);

let page_size = prepared.get_page_size();

Consistency Levels

use scylla::statement::Consistency;
use scylla::statement::SerialConsistency;

let mut prepared = session.prepare("UPDATE ks.table SET v = ? WHERE id = ?").await?;

// Set consistency
prepared.set_consistency(Consistency::Quorum);

// Set serial consistency for LWT
prepared.set_serial_consistency(Some(SerialConsistency::LocalSerial));

Request Timeout

use std::time::Duration;

let mut prepared = session.prepare("SELECT * FROM ks.table").await?;
prepared.set_request_timeout(Some(Duration::from_secs(30)));

Idempotence

let mut prepared = session.prepare("UPDATE ks.table SET v = ? WHERE id = ?").await?;
prepared.set_is_idempotent(true);

Result Metadata Caching

On databases that support it (modern ScyllaDB), you can enable result metadata caching:
let mut prepared = session.prepare("SELECT * FROM ks.table").await?;
prepared.set_use_cached_result_metadata(true);
On databases without result metadata ID extension support (Cassandra, older ScyllaDB versions), enabling use_cached_result_metadata can cause silent data corruption if the schema changes. Only enable this if you’re certain your schema is stable.

Metadata Access

Prepared statements provide access to metadata about bind variables and result columns:

Bind Variable Metadata

// Get column specifications for bind variables
let col_specs = prepared.get_variable_col_specs();
for spec in col_specs.iter() {
    println!("Column: {}, Type: {:?}", spec.name(), spec.typ());
}

// Get partition key indexes
let pk_indexes = prepared.get_variable_pk_indexes();

Result Metadata

// Get current result set column specifications
let col_specs_guard = prepared.get_current_result_set_col_specs();
let col_specs = col_specs_guard.get();

for spec in col_specs.iter() {
    println!("Result column: {}", spec.name());
}

Table Information

// Get keyspace and table name
if let Some(keyspace) = prepared.get_keyspace_name() {
    println!("Keyspace: {}", keyspace);
}

if let Some(table) = prepared.get_table_name() {
    println!("Table: {}", table);
}

// Get full table spec
if let Some(table_spec) = prepared.get_table_spec() {
    println!("Table: {}.{}", table_spec.ks_name(), table_spec.table_name());
}

Token-Aware Routing

Prepared statements enable token-aware routing when partition key values are provided:
// Check if token-aware routing is possible
if prepared.is_token_aware() {
    println!("This statement supports token-aware routing");
}

// Calculate the token for given values
use scylla::serialize::row::SerializeRow;

let values = (42_i32, "key");
if let Some(token) = prepared.calculate_token(&values)? {
    println!("Token: {}", token.value());
}
For token-aware routing to work, all partition key values must be sent as bound values. If partition key columns are hardcoded in the query, token-aware routing cannot be used.

Lightweight Transactions (LWT)

use scylla::statement::SerialConsistency;

let mut prepared = session
    .prepare("UPDATE ks.table SET value = ? WHERE id = ? IF value = ?")
    .await?;

// Set serial consistency for LWT
prepared.set_serial_consistency(Some(SerialConsistency::Serial));

// Check if this is a confirmed LWT (ScyllaDB-specific optimization)
if prepared.is_confirmed_lwt() {
    println!("LWT with deterministic routing");
}

let result = session.execute_unpaged(&prepared, (new_value, id, old_value)).await?;

Statement Repreparation

The driver automatically handles statement repreparation when:
  • Schema is updated
  • Connection to a node is lost and re-established
  • Server invalidates prepared statement cache
You generally don’t need to worry about repreparation, but be aware:
If you alter the schema (add UDT fields, change column types, etc.), you may need to drop and re-prepare statements manually in some cases. See the Altering Schema section in the source documentation for details.

Complete Example

use scylla::client::session::{Session, SessionBuilder};
use scylla::statement::prepared::PreparedStatement;
use scylla::statement::Consistency;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;

#[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.basic \
             (a int, b int, c text, primary key (a, b))",
            &[],
        )
        .await?;

    // Prepare statements
    let insert_prepared = session
        .prepare("INSERT INTO examples_ks.basic (a, b, c) VALUES (?, ?, ?)")  
        .await?;

    let select_prepared = session
        .prepare("SELECT a, b, c FROM examples_ks.basic")
        .await?;

    // Insert data using prepared statement
    for i in 0..10 {
        session
            .execute_unpaged(&insert_prepared, (i, i * 2, format!("text_{}", i)))
            .await?;
    }

    // Query with automatic paging
    let mut rows_stream = session
        .execute_iter(select_prepared, &[])
        .await?
        .rows_stream::<(i32, i32, String)>()?;

    while let Some((a, b, c)) = rows_stream.try_next().await? {
        println!("a: {}, b: {}, c: {}", a, b, c);
    }

    Ok(())
}

See Also

Build docs developers (and LLMs) love