Skip to content

Prepared Statement Cache - User Guide

Prepared Statement Cache - User Guide

Feature: Intelligent Prepared Statement Caching Component: heliosdb-pooling Version: 4.0.0+ Status: Production Ready

Overview

The Prepared Statement Cache is a high-performance LRU-based caching system that reduces SQL parsing overhead by reusing prepared statements. This feature provides 15%+ throughput improvement while maintaining minimal memory overhead.

Key Benefits

  • 15%+ Performance Improvement: Reduced SQL parsing overhead
  • 80%+ Cache Hit Rate: For typical workloads
  • Low Memory Overhead: <100MB for 10,000 statements
  • Thread-Safe: Lock-free design for concurrent access
  • Type-Safe Parameters: Compile-time and runtime validation
  • Automatic Eviction: LRU strategy prevents memory bloat

Quick Start

Basic Usage

use heliosdb_pooling::{
IntelligentConnectionPool, IntelligentPoolConfig, PoolMode,
Parameters, ParameterValue
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create and initialize pool
let config = IntelligentPoolConfig::default();
let pool = IntelligentConnectionPool::new(config, PoolMode::Transaction)?;
pool.initialize().await?;
// 2. Prepare statement (cached automatically)
let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
// 3. Execute with parameters
let params = Parameters::Positional(vec![ParameterValue::Int64(123)]);
pool.execute(&stmt, params).await?;
// 4. Check cache performance
let stats = pool.statement_cache_stats();
println!("Cache hit rate: {:.2}%", stats.hit_rate * 100.0);
pool.shutdown().await;
Ok(())
}

Parameter Types

Supported Types

The cache supports a wide variety of parameter types:

// Null value
ParameterValue::Null
// Boolean
ParameterValue::Bool(true)
// Integers
ParameterValue::Int8(127)
ParameterValue::Int16(32767)
ParameterValue::Int32(2147483647)
ParameterValue::Int64(9223372036854775807)
ParameterValue::UInt8(255)
ParameterValue::UInt16(65535)
ParameterValue::UInt32(4294967295)
ParameterValue::UInt64(18446744073709551615)
// Floating point
ParameterValue::Float32(3.14)
ParameterValue::Float64(3.14159265359)
// String
ParameterValue::String("Hello, World!".to_string())
// Binary data
ParameterValue::Bytes(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F])
// Timestamp
use chrono::Utc;
ParameterValue::Timestamp(Utc::now())
// JSON
ParameterValue::Json(r#"{"key": "value"}"#.to_string())
// Arrays
ParameterValue::Array(vec![
ParameterValue::Int64(1),
ParameterValue::Int64(2),
ParameterValue::Int64(3),
])

Parameter Binding Modes

Positional Parameters

Use $1, $2, $3... in your SQL:

let stmt = pool.prepare(
"SELECT * FROM users WHERE id = $1 AND email = $2"
)?;
let params = Parameters::Positional(vec![
ParameterValue::Int64(123),
ParameterValue::String("user@example.com".to_string()),
]);
pool.execute(&stmt, params).await?;

Named Parameters

Use :name in your SQL:

let stmt = pool.prepare(
"SELECT * FROM users WHERE id = :user_id AND email = :email"
)?;
let mut params_map = HashMap::new();
params_map.insert("user_id".to_string(), ParameterValue::Int64(123));
params_map.insert(
"email".to_string(),
ParameterValue::String("user@example.com".to_string())
);
pool.execute(&stmt, Parameters::Named(params_map)).await?;

Advanced Usage

Reusing Prepared Statements

The cache automatically reuses statements with the same SQL:

// First call - cache miss, statement is prepared
let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
// Subsequent calls - cache hit, same statement returned
let stmt2 = pool.prepare("SELECT * FROM users WHERE id = $1")?;
assert_eq!(stmt.id, stmt2.id); // Same statement ID

Batch Operations

Prepare once, execute many times:

let stmt = pool.prepare("INSERT INTO users (id, name) VALUES ($1, $2)")?;
for i in 0..1000 {
let params = Parameters::Positional(vec![
ParameterValue::Int64(i),
ParameterValue::String(format!("User {}", i)),
]);
pool.execute(&stmt, params).await?;
}
// Check execution count
println!("Executed {} times", stmt.use_count.load(Ordering::Relaxed));

Complex Queries

let stmt = pool.prepare(r#"
WITH recent_orders AS (
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE created_at > :start_date
GROUP BY user_id
)
SELECT u.id, u.name, COALESCE(ro.order_count, 0) as orders
FROM users u
LEFT JOIN recent_orders ro ON u.id = ro.user_id
WHERE u.active = :active
"#)?;
let mut params = HashMap::new();
params.insert(
"start_date".to_string(),
ParameterValue::Timestamp(Utc::now() - Duration::days(30))
);
params.insert("active".to_string(), ParameterValue::Bool(true));
pool.execute(&stmt, Parameters::Named(params)).await?;

Monitoring and Statistics

Cache Performance Metrics

let stats = pool.statement_cache_stats();
println!("Cache Statistics:");
println!(" Total Hits: {}", stats.total_hits);
println!(" Total Misses: {}", stats.total_misses);
println!(" Hit Rate: {:.2}%", stats.hit_rate * 100.0);
println!(" Current Size: {} statements", stats.current_size);
println!(" Max Size: {} statements", stats.max_size);
println!(" Memory Usage: {} MB", stats.total_memory_bytes / 1024 / 1024);
println!(" Avg Statement Size: {:.0} bytes", stats.average_statement_size_bytes);
println!(" Evictions: {}", stats.total_evictions);

Per-Statement Statistics

let stmt_stats = pool.statement_statistics();
for stat in stmt_stats.iter().take(10) {
println!("SQL: {}", stat.sql);
println!(" Use Count: {}", stat.use_count);
println!(" Avg Execution Time: {:.2}ms", stat.average_execution_time_ms);
println!(" Error Count: {}", stat.error_count);
println!(" Last Used: {}", stat.last_used_at);
println!(" Size: {} bytes", stat.size_bytes);
println!();
}

Cache Management

Manual Cache Clearing

// Clear all cached statements
pool.clear_statement_cache();
let stats = pool.statement_cache_stats();
assert_eq!(stats.current_size, 0);

Cache Configuration

The cache is configured when creating the pool:

// Default configuration: 10,000 statements, 100MB max
let pool = IntelligentConnectionPool::new(config, PoolMode::Transaction)?;
// Cache will automatically:
// - Evict LRU entries when full
// - Track memory usage
// - Maintain metrics

Performance Tuning

Optimizing Cache Hit Rate

  1. Prepare Early: Prepare common statements during initialization
pool.initialize().await?;
// Pre-warm cache with common queries
let common_queries = vec![
"SELECT * FROM users WHERE id = $1",
"SELECT * FROM orders WHERE user_id = $1",
"UPDATE users SET last_login = $1 WHERE id = $2",
];
for sql in common_queries {
pool.prepare(sql)?;
}
  1. Use Consistent SQL: Exact SQL match is required for cache hits
// These are different statements (won't share cache entry):
pool.prepare("SELECT * FROM users WHERE id = $1")?; // Statement 1
pool.prepare("SELECT * FROM users WHERE id = $1")?; // Statement 2 (extra space)
pool.prepare("select * from users where id = $1")?; // Statement 3 (different case)
  1. Avoid Dynamic SQL: Use parameters instead of string interpolation
// Bad - creates unique statement each time
let sql = format!("SELECT * FROM users WHERE id = {}", user_id);
pool.prepare(&sql)?;
// Good - reuses cached statement
let sql = "SELECT * FROM users WHERE id = $1";
let stmt = pool.prepare(sql)?;
pool.execute(&stmt, Parameters::Positional(vec![
ParameterValue::Int64(user_id)
])).await?;

Memory Management

Monitor memory usage:

let stats = pool.statement_cache_stats();
if stats.total_memory_bytes > 80 * 1024 * 1024 { // > 80MB
println!("Warning: High cache memory usage");
// Consider clearing cache or reducing workload
}

Error Handling

Parameter Validation Errors

let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
// Wrong number of parameters
let result = stmt.bind(Parameters::Positional(vec![]));
assert!(result.is_err());
// Correct
let result = stmt.bind(Parameters::Positional(vec![
ParameterValue::Int64(123)
]));
assert!(result.is_ok());

SQL Parsing Errors

// Invalid SQL
match pool.prepare("INVALID SQL QUERY") {
Ok(stmt) => { /* ... */ },
Err(e) => eprintln!("Failed to prepare: {}", e),
}

Best Practices

DO

Prepare once, execute many times

let stmt = pool.prepare("INSERT INTO logs VALUES ($1, $2)")?;
for log in logs {
pool.execute(&stmt, log.to_params()).await?;
}

Use parameters for variable data

pool.prepare("SELECT * FROM users WHERE id = $1")?;

Monitor cache performance

if pool.statement_cache_stats().hit_rate < 0.7 {
println!("Low cache hit rate - consider optimizing queries");
}

Handle errors gracefully

match pool.prepare(sql) {
Ok(stmt) => { /* use statement */ },
Err(e) => { /* handle error */ },
}

DON’T

Don’t use string interpolation for parameters

// Bad
let sql = format!("SELECT * FROM users WHERE id = {}", id);

Don’t prepare the same statement repeatedly in a loop

// Bad
for id in ids {
let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
pool.execute(&stmt, params).await?;
}
// Good
let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
for id in ids {
pool.execute(&stmt, params).await?;
}

Don’t ignore cache statistics

// Monitor regularly
let stats = pool.statement_cache_stats();
if stats.hit_rate < 0.5 {
// Investigate why hit rate is low
}

Troubleshooting

Low Cache Hit Rate

Symptoms: Hit rate < 70%

Possible Causes:

  1. Too many unique queries
  2. Dynamic SQL generation
  3. Inconsistent SQL formatting

Solutions:

  • Use parameterized queries
  • Normalize SQL formatting
  • Pre-warm cache with common queries

High Memory Usage

Symptoms: Memory > 100MB for small workload

Possible Causes:

  1. Too many unique statements
  2. Very long SQL queries

Solutions:

  • Clear cache periodically
  • Reduce number of unique queries
  • Monitor eviction statistics

Slow Performance

Symptoms: Slower than expected

Possible Causes:

  1. Cache misses
  2. Complex parameter binding

Solutions:

  • Check cache hit rate
  • Profile execution times
  • Use simpler parameter types when possible

Integration Examples

Web API Endpoint

use axum::{extract::Path, Json};
async fn get_user(
Path(user_id): Path<i64>,
pool: Arc<IntelligentConnectionPool>,
) -> Result<Json<User>, Error> {
let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
let params = Parameters::Positional(vec![ParameterValue::Int64(user_id)]);
pool.execute(&stmt, params).await?;
// ... fetch results ...
Ok(Json(user))
}

Batch Processing

async fn process_batch(
records: Vec<Record>,
pool: &IntelligentConnectionPool,
) -> Result<()> {
let stmt = pool.prepare(
"INSERT INTO processed (id, data, timestamp) VALUES ($1, $2, $3)"
)?;
for record in records {
let params = Parameters::Positional(vec![
ParameterValue::Int64(record.id),
ParameterValue::Json(record.data),
ParameterValue::Timestamp(Utc::now()),
]);
pool.execute(&stmt, params).await?;
}
Ok(())
}

Performance Expectations

Typical Performance Metrics

OperationExpected TimeNotes
Cache Hit5-10μsReturning cached statement
Cache Miss50-100μsIncludes SQL parsing
Parameter Binding1-5μsPer statement
Execution (simulated)100-200μsReal DB execution varies

Expected Cache Hit Rates

Workload TypeExpected Hit Rate
OLTP (repetitive)85-95%
Batch Processing90-99%
Analytics (varied)60-80%
Ad-hoc Queries30-50%

Summary

The Prepared Statement Cache provides significant performance improvements with minimal configuration. Key takeaways:

  1. Automatic Caching: Statements are cached transparently
  2. High Performance: 15%+ throughput improvement
  3. Low Overhead: <100MB for typical workloads
  4. Easy to Use: Simple API with strong type safety
  5. Production Ready: Comprehensive testing and monitoring

For optimal performance:

  • Use parameterized queries
  • Prepare once, execute many
  • Monitor cache statistics
  • Handle errors properly

Related Documentation: