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 valueParameterValue::Null
// BooleanParameterValue::Bool(true)
// IntegersParameterValue::Int8(127)ParameterValue::Int16(32767)ParameterValue::Int32(2147483647)ParameterValue::Int64(9223372036854775807)ParameterValue::UInt8(255)ParameterValue::UInt16(65535)ParameterValue::UInt32(4294967295)ParameterValue::UInt64(18446744073709551615)
// Floating pointParameterValue::Float32(3.14)ParameterValue::Float64(3.14159265359)
// StringParameterValue::String("Hello, World!".to_string())
// Binary dataParameterValue::Bytes(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F])
// Timestampuse chrono::Utc;ParameterValue::Timestamp(Utc::now())
// JSONParameterValue::Json(r#"{"key": "value"}"#.to_string())
// ArraysParameterValue::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 preparedlet stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;
// Subsequent calls - cache hit, same statement returnedlet stmt2 = pool.prepare("SELECT * FROM users WHERE id = $1")?;
assert_eq!(stmt.id, stmt2.id); // Same statement IDBatch 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 countprintln!("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 statementspool.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 maxlet pool = IntelligentConnectionPool::new(config, PoolMode::Transaction)?;
// Cache will automatically:// - Evict LRU entries when full// - Track memory usage// - Maintain metricsPerformance Tuning
Optimizing Cache Hit Rate
- Prepare Early: Prepare common statements during initialization
pool.initialize().await?;
// Pre-warm cache with common querieslet 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)?;}- 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 1pool.prepare("SELECT * FROM users WHERE id = $1")?; // Statement 2 (extra space)pool.prepare("select * from users where id = $1")?; // Statement 3 (different case)- Avoid Dynamic SQL: Use parameters instead of string interpolation
// Bad - creates unique statement each timelet sql = format!("SELECT * FROM users WHERE id = {}", user_id);pool.prepare(&sql)?;
// Good - reuses cached statementlet 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 parameterslet result = stmt.bind(Parameters::Positional(vec![]));assert!(result.is_err());
// Correctlet result = stmt.bind(Parameters::Positional(vec![ ParameterValue::Int64(123)]));assert!(result.is_ok());SQL Parsing Errors
// Invalid SQLmatch 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
// Badlet sql = format!("SELECT * FROM users WHERE id = {}", id);❌ Don’t prepare the same statement repeatedly in a loop
// Badfor id in ids { let stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?; pool.execute(&stmt, params).await?;}
// Goodlet stmt = pool.prepare("SELECT * FROM users WHERE id = $1")?;for id in ids { pool.execute(&stmt, params).await?;}❌ Don’t ignore cache statistics
// Monitor regularlylet 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:
- Too many unique queries
- Dynamic SQL generation
- 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:
- Too many unique statements
- Very long SQL queries
Solutions:
- Clear cache periodically
- Reduce number of unique queries
- Monitor eviction statistics
Slow Performance
Symptoms: Slower than expected
Possible Causes:
- Cache misses
- 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
| Operation | Expected Time | Notes |
|---|---|---|
| Cache Hit | 5-10μs | Returning cached statement |
| Cache Miss | 50-100μs | Includes SQL parsing |
| Parameter Binding | 1-5μs | Per statement |
| Execution (simulated) | 100-200μs | Real DB execution varies |
Expected Cache Hit Rates
| Workload Type | Expected Hit Rate |
|---|---|
| OLTP (repetitive) | 85-95% |
| Batch Processing | 90-99% |
| Analytics (varied) | 60-80% |
| Ad-hoc Queries | 30-50% |
Summary
The Prepared Statement Cache provides significant performance improvements with minimal configuration. Key takeaways:
- Automatic Caching: Statements are cached transparently
- High Performance: 15%+ throughput improvement
- Low Overhead: <100MB for typical workloads
- Easy to Use: Simple API with strong type safety
- Production Ready: Comprehensive testing and monitoring
For optimal performance:
- Use parameterized queries
- Prepare once, execute many
- Monitor cache statistics
- Handle errors properly
Related Documentation: