Database Performance
Database Performance
Learn how to optimize your Solo Kit database for maximum performance. This guide covers query optimization, indexing strategies, and performance monitoring with Convex.
Performance Overview
Solo Kit Performance Architecture
Solo Kit is designed for enterprise-grade database performance with:
- Convex-first optimization: Automatic query optimization and caching
- Real-time subscriptions: Efficient reactive data updates
- Intelligent indexing: Strategic indexes on all major tables
- Edge deployment: Global low-latency data access
- Automatic scaling: Convex handles scaling automatically
Performance Characteristics
Target Performance Metrics:
- Query Response Time: < 50ms for simple queries, < 200ms for complex
- Real-time Updates: < 100ms for subscription updates
- Concurrent Users: Unlimited with Convex's automatic scaling
- Data Volume: Optimized for any scale with proper indexing
Query Optimization
Convex Query Performance
Solo Kit uses optimized query patterns with Convex:
// convex/users.ts
import { v } from 'convex/values';
import { query } from './_generated/server';
// Efficient single record lookup with index
export const getUserById = query({
args: { id: v.id('users') },
handler: async (ctx, args) => {
// Direct ID lookup is always fast
const user = await ctx.db.get(args.id);
return user;
},
});
// Paginated queries for large datasets
export const getUsers = query({
args: {
limit: v.optional(v.number()),
cursor: v.optional(v.string()),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 50;
let query = ctx.db.query('users').order('desc');
const results = await query.take(limit + 1);
const hasMore = results.length > limit;
const users = hasMore ? results.slice(0, -1) : results;
return {
users,
nextCursor: hasMore ? users[users.length - 1]._id : null,
};
},
});
// Efficient count queries
export const getUserCount = query({
handler: async (ctx) => {
const users = await ctx.db.query('users').collect();
return users.length;
},
});Query Performance Best Practices
Always use indexes for filtered queries:
// GOOD: Uses index for email lookup
const user = await ctx.db
.query('users')
.withIndex('by_email', (q) => q.eq('email', userEmail))
.first();
// AVOID: Full table scan without index
const user = await ctx.db
.query('users')
.filter((q) => q.eq(q.field('email'), userEmail))
.first();Optimize SELECT with field selection:
// Select only needed fields (Convex returns full documents by default)
// For large documents, consider splitting into separate tables
export const getUserProfile = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) return null;
// Return only needed fields
return {
id: user._id,
name: user.name,
email: user.email,
};
},
});Efficient JOIN patterns with Convex:
// Optimized data fetching with related records
export const getUserWithPreferences = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) return null;
const preferences = await ctx.db
.query('userPreferences')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.first();
return {
...user,
preferences,
};
},
});Repository Pattern Performance
Solo Kit implements the repository pattern for optimized queries:
// convex/repositories/users.ts
import { v } from 'convex/values';
import { query } from '../_generated/server';
// Optimized single user lookup
export const findById = query({
args: { id: v.id('users') },
handler: async (ctx, args) => {
// Performance: Direct ID lookup is O(1)
return await ctx.db.get(args.id);
},
});
// Efficient paginated listing
export const list = query({
args: {
limit: v.optional(v.number()),
offset: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 10;
// Performance: Always use limits on collections
const users = await ctx.db.query('users').order('desc').take(limit);
return users;
},
});Strategic Indexing
Solo Kit Index Strategy
Solo Kit implements comprehensive indexing across all major tables:
User Table Indexes (convex/schema.ts):
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
role: v.union(v.literal('admin'), v.literal('user')),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_email', ['email']) // Login lookups
.index('by_role', ['role']) // Role filtering
.index('by_createdAt', ['createdAt']), // Sorting
transactions: defineTable({
userId: v.id('users'),
status: v.string(),
type: v.string(),
amount: v.number(),
createdAt: v.number(),
})
.index('by_userId', ['userId']) // User queries
.index('by_status', ['status']) // Status filtering
.index('by_createdAt', ['createdAt']) // Time-based queries
.index('by_type', ['type']), // Transaction type filtering
verificationTokens: defineTable({
token: v.string(),
userId: v.id('users'),
type: v.string(),
expiresAt: v.number(),
})
.index('by_token', ['token']) // Token lookup
.index('by_expiresAt', ['expiresAt']) // Cleanup queries
.index('by_userId_type', ['userId', 'type']), // Composite lookup
});Index Design Principles
Single Field Indexes:
// For exact match queries
.index('by_email', ['email'])
.index('by_createdAt', ['createdAt'])Composite Indexes:
// For multi-field queries
.index('by_userId_type', ['userId', 'type'])
.index('by_status_createdAt', ['status', 'createdAt'])Index Usage in Queries
Using indexes effectively:
// GOOD: Uses single-field index
const user = await ctx.db
.query('users')
.withIndex('by_email', (q) => q.eq('email', email))
.first();
// GOOD: Uses composite index
const tokens = await ctx.db
.query('verificationTokens')
.withIndex('by_userId_type', (q) => q.eq('userId', userId).eq('type', 'email_verification'))
.collect();
// GOOD: Range query on indexed field
const recentUsers = await ctx.db
.query('users')
.withIndex('by_createdAt', (q) => q.gte('createdAt', cutoffDate))
.collect();Real-time Performance
Subscription Optimization
Convex provides automatic real-time subscriptions:
// React component with real-time updates
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function UserList() {
// Automatically updates when data changes
const users = useQuery(api.users.list, { limit: 10 });
if (!users) return <Loading />;
return (
<ul>
{users.map((user) => (
<li key={user._id}>{user.name}</li>
))}
</ul>
);
}Performance benefits:
- Automatic caching: Convex caches query results
- Incremental updates: Only changed data is sent
- Deduplication: Multiple subscriptions share results
Optimizing Subscriptions
// GOOD: Specific queries for better caching
export const getUserById = query({
args: { id: v.id('users') },
handler: async (ctx, args) => ctx.db.get(args.id),
});
// AVOID: Large queries that change frequently
export const getAllUsersWithActivity = query({
handler: async (ctx) => {
// This would update whenever any user or activity changes
const users = await ctx.db.query('users').collect();
const activities = await ctx.db.query('activities').collect();
// ...
},
});Performance Monitoring
Built-in Performance Testing
Solo Kit includes performance monitoring:
# Run performance baseline tests
pnpm perf:baselinePerformance test implementation:
// scripts/performance-baseline.ts
async function testEndpoint(url: string): Promise<PerformanceResult> {
const times: number[] = [];
const testRuns = 5;
for (let i = 0; i < testRuns; i++) {
try {
const start = Date.now();
const response = await fetch(url);
const end = Date.now();
if (response.ok) {
times.push(end - start);
}
} catch (error) {
console.error(`Error testing ${url}:`, error);
}
}
return {
endpoint: url,
averageTime: times.reduce((a, b) => a + b) / times.length,
status: times.length > 0 ? 'success' : 'error',
};
}Query Performance Analysis
Monitor query performance in Convex Dashboard:
- Open https://dashboard.convex.dev
- Navigate to your project
- View "Functions" tab for execution times
- Check "Logs" for slow query warnings
Database Cleanup Performance
Solo Kit includes automated cleanup for performance:
// convex/cleanup.ts
import { internalMutation } from './_generated/server';
export const cleanupExpiredTokens = internalMutation({
handler: async (ctx) => {
const now = Date.now();
// Efficient deletion using index on expiresAt
const expiredTokens = await ctx.db
.query('verificationTokens')
.withIndex('by_expiresAt', (q) => q.lt('expiresAt', now))
.collect();
for (const token of expiredTokens) {
await ctx.db.delete(token._id);
}
if (expiredTokens.length > 0) {
console.info(`Cleaned up ${expiredTokens.length} expired tokens`);
}
return expiredTokens.length;
},
});Advanced Optimization
Query Batching
Batch multiple queries for efficiency:
// convex/dashboard.ts
export const getUserDashboardData = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
// Execute queries in parallel
const [user, preferences, subscriptions, recentTransactions] = await Promise.all([
ctx.db.get(args.userId),
ctx.db
.query('userPreferences')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.first(),
ctx.db
.query('subscriptions')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.collect(),
ctx.db
.query('transactions')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.order('desc')
.take(10),
]);
return {
user,
preferences,
subscriptions,
recentTransactions,
};
},
});Pagination Optimization
Cursor-based pagination for large datasets:
// convex/users.ts
export const getUsersWithCursor = query({
args: {
cursor: v.optional(v.id('users')),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 20;
let query = ctx.db.query('users').order('desc');
if (args.cursor) {
// Start after the cursor
const cursorDoc = await ctx.db.get(args.cursor);
if (cursorDoc) {
query = query.filter((q) => q.lt(q.field('_creationTime'), cursorDoc._creationTime));
}
}
const results = await query.take(limit + 1);
const hasNextPage = results.length > limit;
const users = hasNextPage ? results.slice(0, -1) : results;
const nextCursor = hasNextPage ? users[users.length - 1]._id : null;
return {
users,
nextCursor,
hasNextPage,
};
},
});Caching Strategy
Leverage Convex's built-in caching:
// Convex automatically caches query results
// Re-running the same query returns cached data instantly
// For expensive computations, consider storing computed values
export const updateUserStats = mutation({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
// Compute and cache statistics
const transactions = await ctx.db
.query('transactions')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.collect();
const stats = {
totalTransactions: transactions.length,
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
lastUpdated: Date.now(),
};
// Store computed stats for fast retrieval
await ctx.db.patch(args.userId, { cachedStats: stats });
},
});Performance Monitoring
Database Metrics
Key metrics to monitor:
- Query response time: Average and P95 response times
- Subscription count: Active real-time subscriptions
- Function execution time: Time spent in Convex functions
- Error rate: Failed queries and mutations
Convex Dashboard monitoring:
- Functions tab: Execution times, invocation counts
- Logs tab: Errors, warnings, and custom logs
- Data tab: Document counts and storage usage
Performance Alerting
Set up monitoring alerts:
// convex/monitoring.ts
export const checkDatabasePerformance = query({
handler: async (ctx) => {
const startTime = Date.now();
// Test query performance
await ctx.db.query('users').take(1);
const responseTime = Date.now() - startTime;
// Log slow queries
if (responseTime > 1000) {
console.warn(`Slow query detected: ${responseTime}ms`);
}
return {
healthy: responseTime < 1000,
responseTime,
};
},
});Development Performance
Local Development Optimization
# Start Convex dev server with hot reload
npx convex dev
# Monitor function performance in dashboard
# Visit: https://dashboard.convex.dev
# Run performance baseline tests
pnpm perf:baselineProduction Performance
# Deploy optimized functions
npx convex deploy
# Monitor production health
curl /api/health/database
# Check general application health
curl /api/healthzNext Steps
Master database optimization with these advanced guides:
- Security - Secure database operations
- Monitoring - Comprehensive database monitoring
- Backup & Restore - Data protection strategies
- Advanced - Advanced Convex features
Optimized database performance ensures your Solo Kit application scales efficiently and provides excellent user experience.