Performance Optimization
This guide covers strategies for optimizing d1-rs applications for high performance, both in Cloudflare Workers and local development.
Database Design
Indexing Strategy
Proper indexing is crucial for query performance:
// Migration with strategic indexes
let migration = SchemaMigration::new("optimize_performance".to_string())
.create_table("users")
.integer("id").primary_key().auto_increment().build()
.text("email").not_null().unique().build()
.text("name").not_null().build()
.boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
.datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
.datetime("last_login").build()
.build()
// Add strategic indexes
.alter_table("users")
// Single column indexes
.add_index("idx_users_email", vec!["email"])
.add_index("idx_users_active", vec!["is_active"])
.add_index("idx_users_created", vec!["created_at"])
.add_index("idx_users_last_login", vec!["last_login"])
// Composite indexes for common query patterns
.add_index("idx_users_active_created", vec!["is_active", "created_at"])
.add_index("idx_users_active_name", vec!["is_active", "name"])
.build()
.create_table("posts")
.integer("id").primary_key().auto_increment().build()
.integer("user_id").not_null().build()
.text("title").not_null().build()
.text("content").not_null().build()
.boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
.datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
.datetime("published_at").build()
.build()
.alter_table("posts")
// Foreign key index
.add_index("idx_posts_user_id", vec!["user_id"])
// Query-specific indexes
.add_index("idx_posts_published", vec!["is_published"])
.add_index("idx_posts_published_at", vec!["published_at"])
// Composite indexes for common queries
.add_index("idx_posts_user_published", vec!["user_id", "is_published"])
.add_index("idx_posts_published_created", vec!["is_published", "created_at"])
.build();
Denormalization Strategies
Strategic denormalization can improve read performance:
#[derive(Entity, RelationalEntity)]
pub struct OptimizedUser {
#[primary_key]
pub id: i64,
pub name: String,
pub email: String,
pub is_active: bool,
// Denormalized counters
pub post_count: i64,
pub published_post_count: i64,
pub last_post_date: Option<DateTime<Utc>>,
// Cached derived data
pub activity_score: f64, // Calculated based on posts, comments, etc.
pub stats_updated_at: DateTime<Utc>,
}
impl OptimizedUser {
// Update denormalized data
pub async fn refresh_stats(&self, db: &D1Client) -> Result<OptimizedUser> {
let posts = self.traverse::<Post>(db, "posts").await?;
let published_posts: Vec<_> = posts.iter()
.filter(|p| p.is_published)
.collect();
let last_post_date = posts.iter()
.map(|p| p.created_at)
.max();
// Calculate activity score (example algorithm)
let activity_score = self.calculate_activity_score(&posts).await;
OptimizedUser::update(self.id)
.set_post_count(posts.len() as i64)
.set_published_post_count(published_posts.len() as i64)
.set_last_post_date(last_post_date)
.set_activity_score(activity_score)
.set_stats_updated_at(Utc::now())
.save(db)
.await
}
async fn calculate_activity_score(&self, posts: &[Post]) -> f64 {
// Example: Score based on recency and volume
let now = Utc::now();
let mut score = 0.0;
for post in posts {
let days_old = (now - post.created_at).num_days() as f64;
let recency_factor = 1.0 / (1.0 + days_old / 30.0); // Decay over 30 days
if post.is_published {
score += 2.0 * recency_factor;
} else {
score += 1.0 * recency_factor;
}
}
score
}
// Fast queries using denormalized data
pub async fn most_active_users(db: &D1Client, limit: i64) -> Result<Vec<OptimizedUser>> {
OptimizedUser::query()
.where_is_active_eq(true)
.order_by_activity_score_desc()
.limit(limit)
.all(db)
.await
}
pub async fn recently_active_users(db: &D1Client, days: i64) -> Result<Vec<OptimizedUser>> {
let cutoff = Utc::now() - Duration::days(days);
OptimizedUser::query()
.where_last_post_date_gte(cutoff)
.order_by_last_post_date_desc()
.all(db)
.await
}
}
Query Optimization
Efficient Query Patterns
Write queries that minimize database work:
// Good: Specific queries
impl Post {
pub async fn recent_published(db: &D1Client, days: i64, limit: i64) -> Result<Vec<Post>> {
let cutoff = Utc::now() - Duration::days(days);
Post::query()
.where_is_published_eq(true)
.where_created_at_gte(cutoff)
.order_by_created_at_desc()
.limit(limit)
.all(db)
.await
}
// Avoid: Loading unnecessary data
// pub async fn all_posts_then_filter() -> Result<Vec<Post>> {
// let all_posts = Post::query().all(db).await?;
// let filtered: Vec<_> = all_posts.into_iter()
// .filter(|p| p.is_published)
// .take(10)
// .collect();
// Ok(filtered)
// }
}
Pagination Best Practices
Implement efficient pagination:
#[derive(Debug, Serialize)]
pub struct PaginatedResult<T> {
pub data: Vec<T>,
pub page: i64,
pub per_page: i64,
pub total_count: Option<i64>,
pub has_next: bool,
}
impl<T> PaginatedResult<T> {
pub fn new(data: Vec<T>, page: i64, per_page: i64, has_next: bool) -> Self {
Self {
data,
page,
per_page,
total_count: None,
has_next,
}
}
pub fn with_total_count(mut self, total_count: i64) -> Self {
self.total_count = Some(total_count);
self
}
}
impl Post {
pub async fn paginated(
db: &D1Client,
page: i64,
per_page: i64,
include_total: bool,
) -> Result<PaginatedResult<Post>> {
let offset = page * per_page;
// Load one extra to check if there's a next page
let posts = Post::query()
.where_is_published_eq(true)
.order_by_created_at_desc()
.limit(per_page + 1)
.offset(offset)
.all(db)
.await?;
let has_next = posts.len() > per_page as usize;
let mut data = posts;
if has_next {
data.pop(); // Remove the extra item
}
let mut result = PaginatedResult::new(data, page, per_page, has_next);
if include_total {
let total_count = Post::query()
.where_is_published_eq(true)
.count(db)
.await?;
result = result.with_total_count(total_count);
}
Ok(result)
}
// Cursor-based pagination (more efficient for large datasets)
pub async fn paginated_cursor(
db: &D1Client,
cursor: Option<i64>, // Last seen ID
limit: i64,
) -> Result<PaginatedResult<Post>> {
let mut query = Post::query()
.where_is_published_eq(true)
.order_by_id_desc()
.limit(limit + 1);
if let Some(cursor_id) = cursor {
query = query.where_id_lt(cursor_id);
}
let posts = query.all(db).await?;
let has_next = posts.len() > limit as usize;
let mut data = posts;
if has_next {
data.pop();
}
Ok(PaginatedResult::new(data, 0, limit, has_next))
}
}
Batch Operations
Optimize bulk operations:
impl User {
// Efficient bulk user creation
pub async fn create_bulk(db: &D1Client, user_data: Vec<CreateUserData>) -> Result<Vec<User>> {
let mut created_users = Vec::new();
// In a real implementation with transactions:
// db.transaction(|tx| async {
// for data in user_data {
// let user = User::create()
// .set_name(data.name)
// .set_email(data.email)
// .save(&tx)
// .await?;
// created_users.push(user);
// }
// Ok(created_users)
// }).await
// Current implementation
for data in user_data {
let user = User::create()
.set_name(data.name)
.set_email(data.email)
.set_is_active(true)
.save(db)
.await?;
created_users.push(user);
}
Ok(created_users)
}
// Efficient bulk updates
pub async fn activate_users_bulk(db: &D1Client, user_ids: Vec<i64>) -> Result<Vec<User>> {
let mut updated_users = Vec::new();
for user_id in user_ids {
let updated = User::update(user_id)
.set_is_active(true)
.save(db)
.await?;
updated_users.push(updated);
}
Ok(updated_users)
}
}
#[derive(Debug)]
pub struct CreateUserData {
pub name: String,
pub email: String,
}
Memory Management
Lazy Loading
Avoid loading unnecessary data:
impl User {
// Lazy-loaded relationships
pub async fn recent_posts_lazy(&self, db: &D1Client, limit: i64) -> Result<Vec<Post>> {
// Only load what we need
Post::query()
.where_user_id_eq(self.id)
.order_by_created_at_desc()
.limit(limit)
.all(db)
.await
}
// Avoid: Eager loading everything
// pub async fn all_data_eager(&self, db: &D1Client) -> Result<UserWithEverything> {
// let posts = self.traverse::<Post>(db, "posts").await?;
// let profile = self.traverse::<Profile>(db, "profile").await?;
// let comments = self.traverse::<Comment>(db, "comments").await?;
// // This loads potentially massive amounts of data
// }
}
Selective Field Loading
Load only required fields (future feature):
// This pattern will be supported in future versions
// #[derive(Debug, Serialize, Deserialize)]
// pub struct UserSummary {
// pub id: i64,
// pub name: String,
// pub is_active: bool,
// }
//
// impl User {
// pub async fn summaries(db: &D1Client, limit: i64) -> Result<Vec<UserSummary>> {
// // Future: SELECT id, name, is_active FROM users LIMIT ?
// // For now: Load full entities and transform
// let users = User::query().limit(limit).all(db).await?;
// let summaries = users.into_iter().map(|u| UserSummary {
// id: u.id,
// name: u.name,
// is_active: u.is_active,
// }).collect();
// Ok(summaries)
// }
// }
Cloudflare Workers Optimization
Edge-Specific Patterns
Optimize for the Workers runtime:
use worker::*;
// Efficient handler pattern
#[event(fetch)]
async fn main(req: Request, env: Env, ctx: worker::Context) -> Result<Response> {
let db = D1Client::new(env.d1("DB")?);
match req.method() {
Method::Get => {
match req.path().as_str() {
"/users" => handle_get_users(&db, &req).await,
"/posts" => handle_get_posts(&db, &req).await,
path if path.starts_with("/users/") => handle_get_user(&db, path).await,
_ => Response::error("Not found", 404),
}
}
Method::Post => {
match req.path().as_str() {
"/users" => handle_create_user(&db, req).await,
"/posts" => handle_create_post(&db, req).await,
_ => Response::error("Not found", 404),
}
}
_ => Response::error("Method not allowed", 405),
}
}
async fn handle_get_users(db: &D1Client, req: &Request) -> Result<Response> {
// Parse query parameters for pagination
let url = req.url()?;
let page = url.search_params()
.get("page")
.and_then(|p| p.parse::<i64>().ok())
.unwrap_or(0);
let per_page = url.search_params()
.get("per_page")
.and_then(|p| p.parse::<i64>().ok())
.unwrap_or(10)
.min(100); // Limit max page size
// Use efficient pagination
let result = User::paginated(db, page, per_page, false).await
.map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
Response::from_json(&result)
}
async fn handle_get_posts(db: &D1Client, req: &Request) -> Result<Response> {
let url = req.url()?;
// Check for cursor-based pagination
if let Some(cursor) = url.search_params().get("cursor") {
let cursor_id = cursor.parse::<i64>()
.map_err(|_| worker::Error::BadRequest)?;
let limit = url.search_params()
.get("limit")
.and_then(|l| l.parse::<i64>().ok())
.unwrap_or(20)
.min(100);
let result = Post::paginated_cursor(db, Some(cursor_id), limit).await
.map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
Response::from_json(&result)
} else {
// Regular pagination
let page = url.search_params()
.get("page")
.and_then(|p| p.parse::<i64>().ok())
.unwrap_or(0);
let result = Post::paginated(db, page, 20, false).await
.map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
Response::from_json(&result)
}
}
Response Caching
Cache responses at the edge:
async fn handle_popular_posts(db: &D1Client, ctx: &worker::Context) -> Result<Response> {
let cache = ctx.env.cf().unwrap().cache();
let cache_key = "popular_posts_v1";
// Try cache first
if let Some(cached_response) = cache.get(cache_key, CacheOptions::default()).await {
return Ok(cached_response);
}
// Load from database
let posts = Post::query()
.where_is_published_eq(true)
.order_by_created_at_desc()
.limit(10)
.all(db)
.await
.map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
let response = Response::from_json(&posts)?;
// Cache for 5 minutes
cache.put(cache_key, response.clone(), CacheTtl::Minutes(5)).await;
Ok(response)
}
Monitoring and Profiling
Performance Metrics
Track key performance indicators:
use std::time::Instant;
pub struct QueryMetrics {
pub query_time: std::time::Duration,
pub result_count: usize,
pub query_type: String,
}
impl User {
pub async fn find_with_metrics(db: &D1Client, id: i64) -> Result<(Option<User>, QueryMetrics)> {
let start = Instant::now();
let user = User::find(db, id).await?;
let metrics = QueryMetrics {
query_time: start.elapsed(),
result_count: if user.is_some() { 1 } else { 0 },
query_type: "find_user".to_string(),
};
// Log metrics (in Workers, use console.log)
console_log!(
"Query: {} took {:?} and returned {} results",
metrics.query_type,
metrics.query_time,
metrics.result_count
);
Ok((user, metrics))
}
}
// Middleware pattern for automatic metrics
pub async fn with_metrics<F, T>(
operation_name: &str,
operation: F,
) -> Result<(T, QueryMetrics)>
where
F: std::future::Future<Output = Result<T>>,
T: std::fmt::Debug,
{
let start = Instant::now();
let result = operation.await?;
let metrics = QueryMetrics {
query_time: start.elapsed(),
result_count: 1, // Would need to be calculated based on T
query_type: operation_name.to_string(),
};
console_log!("Operation {} completed in {:?}", operation_name, metrics.query_time);
Ok((result, metrics))
}
Query Analysis
Analyze and optimize slow queries:
pub struct SlowQueryLogger;
impl SlowQueryLogger {
pub fn log_if_slow(query_name: &str, duration: std::time::Duration, threshold_ms: u64) {
if duration.as_millis() > threshold_ms as u128 {
console_log!(
"SLOW QUERY: {} took {}ms (threshold: {}ms)",
query_name,
duration.as_millis(),
threshold_ms
);
}
}
}
impl User {
pub async fn complex_query_with_logging(db: &D1Client) -> Result<Vec<User>> {
let start = Instant::now();
let users = User::query()
.where_is_active_eq(true)
.order_by_name_asc()
.all(db)
.await?;
SlowQueryLogger::log_if_slow("complex_user_query", start.elapsed(), 100);
Ok(users)
}
}
Best Practices Summary
Do's
- Index frequently queried columns
- Use pagination for large result sets
- Load only necessary data
- Batch operations when possible
- Cache expensive computations
- Monitor query performance
Don'ts
- Don't load all data then filter in Rust
- Don't ignore database indexes
- Don't perform N+1 queries
- Don't load unnecessary relationship data
- Don't forget to limit query results
Cloudflare Workers Specific
- Use edge caching for static data
- Implement request coalescing for similar queries
- Monitor cold start times
- Optimize bundle size
Next Steps
- Learn about Testing Strategies for performance testing
- Explore Boolean Handling for type conversion optimization
- Check out Migrations for evolving schemas efficiently